From 2f2720f1c1c02256b8c89f0735394562a997ea7a Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 28 Mar 2023 17:59:05 -0700 Subject: [PATCH 001/234] Remove constructor parameter argument definition. --- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 112 +++++++------- .../CommandLineParserNullableTest.cs | 38 +++-- .../CommandLineParserTest.cs | 70 +++------ .../ArgumentNameAttribute.cs | 139 ------------------ src/Ookii.CommandLine/CommandLineArgument.cs | 53 ------- .../CommandLineConstructorAttribute.cs | 19 --- src/Ookii.CommandLine/CommandLineParser.cs | 117 +++------------ src/Ookii.CommandLine/ParseOptions.cs | 18 +-- .../ParseOptionsAttribute.cs | 10 +- .../ValueDescriptionAttribute.cs | 64 -------- 10 files changed, 135 insertions(+), 505 deletions(-) delete mode 100644 src/Ookii.CommandLine/ArgumentNameAttribute.cs delete mode 100644 src/Ookii.CommandLine/CommandLineConstructorAttribute.cs delete mode 100644 src/Ookii.CommandLine/ValueDescriptionAttribute.cs diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index fe2a7dd8..05715fa0 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -20,33 +20,27 @@ class TestArguments private readonly Collection _arg12 = new Collection(); private readonly Dictionary _arg14 = new Dictionary(); - private TestArguments(string notAnArg) - { - } - - public TestArguments([Description("Arg1 description.")] string arg1, [Description("Arg2 description."), ArgumentName("other"), ValueDescription("Number")] int arg2 = 42, bool notSwitch = false) - { - Arg1 = arg1; - Arg2 = arg2; - NotSwitch = notSwitch; - } - - public string Arg1 { get; private set; } + [CommandLineArgument("arg1", Position = 1, IsRequired = true)] + [Description("Arg1 description.")] + public string Arg1 { get; set; } - public int Arg2 { get; private set; } + [CommandLineArgument("other", Position = 2, DefaultValue = 42, ValueDescription = "Number")] + [Description("Arg2 description.")] + public int Arg2 { get; set; } - public bool NotSwitch { get; private 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", ValueDescription = "Number", Position = 1), Description("Arg4 description.")] + [CommandLineArgument("other2", DefaultValue = "47", ValueDescription = "Number", Position = 5), Description("Arg4 description.")] [ValidateRange(0, 1000, IncludeInUsageHelp = false)] public int Arg4 { get; set; } // Short/long name stuff should be ignored if not using LongShort mode. - [CommandLineArgument(Position = 0, ShortName = 'a', IsLong = false), Description("Arg5 description.")] + [CommandLineArgument(Position = 4, ShortName = 'a', IsLong = false), Description("Arg5 description.")] public float Arg5 { get; set; } [Alias("Alias1")] @@ -58,7 +52,7 @@ public TestArguments([Description("Arg1 description.")] string arg1, [Descriptio [CommandLineArgument()] public bool Arg7 { get; set; } - [CommandLineArgument(Position = 2)] + [CommandLineArgument(Position = 6)] public DayOfWeek[] Arg8 { get; set; } [CommandLineArgument()] @@ -98,21 +92,10 @@ public IDictionary Arg14 public static string NotAnArg3 { get; set; } } - class MultipleConstructorsArguments + class ThrowingArguments { private int _throwingArgument; - 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] public int ThrowingArgument { @@ -129,6 +112,17 @@ public int ThrowingArgument } } + class ThrowingConstructor + { + public ThrowingConstructor() + { + throw new ArgumentException(); + } + + [CommandLineArgument] + public int Arg { get; set; } + } + class DictionaryArguments { [CommandLineArgument] @@ -202,18 +196,11 @@ class CultureArguments [ParseOptions(Mode = ParsingMode.LongShort)] class LongShortArguments { - 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; } - [CommandLineArgument(ShortName = 'a', Position = 0), ShortAlias('b'), Alias("baz")] + [CommandLineArgument(ShortName = 'a', Position = 2), ShortAlias('b'), Alias("baz")] [Description("Arg2 description.")] public int Arg2 { get; set; } @@ -229,8 +216,12 @@ public LongShortArguments([ArgumentName(IsShort = true), Description("Foo descri [Description("Switch3 description.")] public bool Switch3 { get; set; } + [CommandLineArgument("foo", Position = 0, IsShort = true, DefaultValue = 0)] + [Description("Foo description.")] public int Foo { get; set; } + [CommandLineArgument("bar", DefaultValue = 0, Position = 1)] + [Description("Bar description.")] public int Bar { get; set; } } @@ -336,9 +327,8 @@ class HiddenArguments class NameTransformArguments { - public NameTransformArguments(string testArg) - { - } + [CommandLineArgument(Position = 0, IsRequired = true)] + public string testArg { get; set; } [CommandLineArgument] public int TestArg2 { get; set; } @@ -363,16 +353,13 @@ class ValidationArguments { public static int Arg3Value { get; set; } - public ValidationArguments([ValidateNotEmpty, Description("Arg2 description.")] string arg2 = null) - { - Arg2 = arg2; - } - [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] @@ -473,28 +460,29 @@ public InjectionArguments(CommandLineParser parser) public int Arg { get; set; } } - class InjectionMixedArguments - { - private readonly CommandLineParser _parser; - private readonly int _arg1; - private readonly int _arg2; + // TODO: Test with new ctor argument style. + //class InjectionMixedArguments + //{ + // private readonly CommandLineParser _parser; + // private readonly int _arg1; + // private readonly int _arg2; - public InjectionMixedArguments(int arg1, CommandLineParser parser, int arg2) - { - _arg1 = arg1; - _parser = parser; - _arg2 = arg2; - } + // public InjectionMixedArguments(int arg1, CommandLineParser parser, int arg2) + // { + // _arg1 = arg1; + // _parser = parser; + // _arg2 = arg2; + // } - public CommandLineParser Parser => _parser; + // public CommandLineParser Parser => _parser; - public int Arg1 => _arg1; + // public int Arg1 => _arg1; - public int Arg2 => _arg2; + // public int Arg2 => _arg2; - [CommandLineArgument] - public int Arg3 { get; set; } - } + // [CommandLineArgument] + // public int Arg3 { get; set; } + //} struct StructWithParseCulture { diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs index 821a7482..2c7b9473 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -79,21 +79,33 @@ public override bool CanConvertFrom(ITypeDescriptorContext? context, Type source 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; - } - + // TODO: Put back with new ctor approach. + //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; + //} + + [CommandLineArgument("constructorNullable", Position = 0)] + [TypeConverter(typeof(NullReturningStringConverter))] public string? ConstructorNullable { get; set; } - public string ConstructorNonNullable { get; set; } + + [CommandLineArgument("constructorNonNullable", Position = 1)] + [TypeConverter(typeof(NullReturningStringConverter))] + public string ConstructorNonNullable { get; set; } = default!; + + [CommandLineArgument("constructorValueType", Position = 2)] + [TypeConverter(typeof(NullReturningIntConverter))] public int ConstructorValueType { get; set; } + + [CommandLineArgument("constructorNullableValueType", Position = 3)] + [TypeConverter(typeof(NullReturningIntConverter))] public int? ConstructorNullableValueType { get; set; } [CommandLineArgument] diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 64f4a798..8c5ed908 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -67,9 +67,9 @@ public void ConstructorTest() 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("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." }, 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 }, @@ -88,29 +88,6 @@ public void ConstructorTest() }); } - [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 ParseTest() { @@ -158,7 +135,7 @@ public void ParseTestEmptyArguments() [TestMethod] public void ParseTestTooManyArguments() { - Type argumentsType = typeof(MultipleConstructorsArguments); + Type argumentsType = typeof(ThrowingArguments); var options = new ParseOptions() { ArgumentNamePrefixes = new[] { "/", "-" } @@ -173,7 +150,7 @@ public void ParseTestTooManyArguments() [TestMethod] public void ParseTestPropertySetterThrows() { - Type argumentsType = typeof(MultipleConstructorsArguments); + Type argumentsType = typeof(ThrowingArguments); var options = new ParseOptions() { ArgumentNamePrefixes = new[] { "/", "-" } @@ -181,7 +158,7 @@ public void ParseTestPropertySetterThrows() var target = new CommandLineParser(argumentsType, options); - CheckThrows(() => target.Parse(new[] { "Foo", "-ThrowingArgument", "-5" }), + CheckThrows(() => target.Parse(new[] { "-ThrowingArgument", "-5" }), target, CommandLineArgumentErrorCategory.ApplyValueError, "ThrowingArgument", @@ -191,7 +168,7 @@ public void ParseTestPropertySetterThrows() [TestMethod] public void ParseTestConstructorThrows() { - Type argumentsType = typeof(MultipleConstructorsArguments); + Type argumentsType = typeof(ThrowingConstructor); var options = new ParseOptions() { ArgumentNamePrefixes = new[] { "/", "-" } @@ -199,7 +176,7 @@ public void ParseTestConstructorThrows() var target = new CommandLineParser(argumentsType, options); - CheckThrows(() => target.Parse(new[] { "invalid" }), + CheckThrows(() => target.Parse(Array.Empty()), target, CommandLineArgumentErrorCategory.CreateArgumentsTypeError, null, @@ -1059,21 +1036,22 @@ public void TestMultiValueWhiteSpaceSeparator() CheckThrows(() => parser.Parse(new[] { "1", "-Multi:2", "2", "3", "4", "-Other", "5", "6" }), parser, CommandLineArgumentErrorCategory.TooManyArguments); } - [TestMethod] - public void TestInjection() - { - 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); - } + // TODO: + //[TestMethod] + //public void TestInjection() + //{ + // 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); + //} [TestMethod] public void TestDuplicateArguments() 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/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index dc6af731..1a255cea 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -423,7 +423,6 @@ public string MemberName /// /// /// - /// public string ArgumentName => _argumentName; /// @@ -438,7 +437,6 @@ public string MemberName /// /// /// - /// public char ShortName => _shortName; /// @@ -525,7 +523,6 @@ public string? ShortNameWithPrefix /// /// /// - /// public bool HasShortName => _shortName != '\0'; /// @@ -541,7 +538,6 @@ public string? ShortNameWithPrefix /// /// /// - /// public bool HasLongName => _hasLongName; /// @@ -1238,55 +1234,6 @@ internal bool SetValue(CultureInfo culture, string? value) return continueParsing; } - internal static CommandLineArgument Create(CommandLineParser parser, ParameterInfo parameter) - { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } - - if (parameter?.Name == null) - { - throw new ArgumentNullException(nameof(parameter)); - } - - 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(), - }; - - return new CommandLineArgument(info); - } - internal static CommandLineArgument Create(CommandLineParser parser, PropertyInfo property) { if (parser == null) 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..01781c01 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -20,20 +20,16 @@ namespace Ookii.CommandLine /// /// /// 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. + /// values. Which arguments are accepted is determined from 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. /// /// - /// 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. - /// - /// - /// 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 arguments type must have a constructor that has no parameter, or a single parameter + /// with the type , which will be passed the instance of the + /// class that was used to parse the arguments when the type + /// is instantiated. /// /// /// A property defines a command line argument if it is , not @@ -164,8 +160,6 @@ private struct PrefixInfo // 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; @@ -176,7 +170,6 @@ private struct PrefixInfo 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. @@ -307,17 +300,11 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) _sortedPrefixes = prefixInfos.OrderByDescending(info => info.Prefix.Length).ToArray(); _argumentsByName = new(comparer); - _commandLineConstructor = GetCommandLineConstructor(); - DetermineConstructorArguments(); - _constructorArgumentCount = _arguments.Count; - _positionalArgumentCount = _constructorArgumentCount + DetermineMemberArguments(options, optionsAttribute); + _positionalArgumentCount = 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)); - } + // Sort the member arguments in usage order (positional first, then required + // non-positional arguments, then the rest by name. + _arguments.Sort(new CommandLineArgumentComparer(_argumentsByName.Comparer)); VerifyPositionalArgumentRules(); } @@ -1208,25 +1195,6 @@ private static string[] DetermineArgumentNamePrefixes(ParseOptions options) } } - private void DetermineConstructorArguments() - { - ParameterInfo[] parameters = _commandLineConstructor.GetParameters(); - var valueDescriptionTransform = _parseOptions.ValueDescriptionTransform ?? default; - - foreach (ParameterInfo parameter in parameters) - { - if (parameter.ParameterType == typeof(CommandLineParser) && _injectionIndex < 0) - { - _injectionIndex = _arguments.Count; - } - else - { - var argument = CommandLineArgument.Create(this, parameter); - AddNamedArgument(argument); - } - } - } - private int DetermineMemberArguments(ParseOptions? options, ParseOptionsAttribute? optionsAttribute) { var valueDescriptionTransform = options?.ValueDescriptionTransform ?? optionsAttribute?.ValueDescriptionTransform @@ -1429,28 +1397,9 @@ private void VerifyPositionalArgumentRules() validator.Validate(this); } - var count = _constructorArgumentCount; - if (_injectionIndex >= 0) - { - ++count; - } - - var constructorArgumentValues = new object?[count]; - int offset = 0; - for (int x = 0; x < count; ++x) - { - if (x == _injectionIndex) - { - constructorArgumentValues[x] = this; - offset = 1; - } - else - { - constructorArgumentValues[x] = _arguments[x - offset].Value; - } - } - - object commandLineArguments = CreateArgumentsTypeInstance(constructorArgumentValues); + // TODO: Integrate with new ctor argument support. + var inject = _argumentsType.GetConstructor(new[] { typeof(CommandLineParser) }) != null; + object commandLineArguments = CreateArgumentsTypeInstance(inject); foreach (CommandLineArgument argument in _arguments) { // Apply property argument values (this does nothing for constructor or method arguments). @@ -1610,36 +1559,18 @@ private CommandLineArgument GetShortArgumentOrThrow(string shortName) return null; } - private ConstructorInfo GetCommandLineConstructor() - { - ConstructorInfo[] ctors = _argumentsType.GetConstructors(); - if (ctors.Length < 1) - { - throw new NotSupportedException(Properties.Resources.NoConstructor); - } - 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) + private object CreateArgumentsTypeInstance(bool inject) { try { - return _commandLineConstructor.Invoke(constructorArgumentValues); + if (inject) + { + return Activator.CreateInstance(_argumentsType, this)!; + } + else + { + return Activator.CreateInstance(_argumentsType)!; + } } catch (TargetInvocationException ex) { diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 80fdfc06..9f277a8b 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -67,10 +67,8 @@ public class ParseOptions /// /// /// 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. + /// property set, the argument name is determined by taking the name of the property, or + /// method that defines it, and applying the specified transform. /// /// /// The name transform will also be applied to the names of the automatically added @@ -452,10 +450,10 @@ public LocalizedStringProvider StringProvider /// /// /// 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. + /// property set, 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. /// /// /// @@ -473,8 +471,8 @@ public LocalizedStringProvider StringProvider /// /// /// This property has no effect on explicit value description specified with the - /// property, the - /// attribute, or the property. + /// property or the + /// property. /// /// /// If not , this property overrides the diff --git a/src/Ookii.CommandLine/ParseOptionsAttribute.cs b/src/Ookii.CommandLine/ParseOptionsAttribute.cs index c12d8e56..7f66f0eb 100644 --- a/src/Ookii.CommandLine/ParseOptionsAttribute.cs +++ b/src/Ookii.CommandLine/ParseOptionsAttribute.cs @@ -56,10 +56,8 @@ public class ParseOptionsAttribute : Attribute /// /// /// 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. + /// 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 @@ -297,8 +295,8 @@ public class ParseOptionsAttribute : Attribute /// /// /// This property has no effect on explicit value description specified with the - /// property, the - /// attribute, or the property. + /// property or the + /// property. /// /// /// This value can be overridden by the diff --git a/src/Ookii.CommandLine/ValueDescriptionAttribute.cs b/src/Ookii.CommandLine/ValueDescriptionAttribute.cs deleted file mode 100644 index 8d823b7a..00000000 --- a/src/Ookii.CommandLine/ValueDescriptionAttribute.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; -using System.ComponentModel; - -namespace Ookii.CommandLine -{ - /// - /// Provides a custom value description for use in the usage help for an argument created from a constructor parameter. - /// - /// - /// - /// 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 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; } - } - } -} From af1f6b0840c8296a82c7743e12f34c266c00ac73 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 29 Mar 2023 10:07:13 -0700 Subject: [PATCH 002/234] Start on new converters. --- .../Conversion/ArgumentConverter.cs | 17 +++++++ .../Conversion/ParseConverter.cs | 49 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/Ookii.CommandLine/Conversion/ArgumentConverter.cs create mode 100644 src/Ookii.CommandLine/Conversion/ParseConverter.cs diff --git a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs new file mode 100644 index 00000000..e4a8c605 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs @@ -0,0 +1,17 @@ +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion +{ + public abstract class ArgumentConverter + { + public abstract object? Convert(string value, CultureInfo culture); + +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public virtual object? Convert(ReadOnlySpan value, CultureInfo culture) + { + return Convert(value.ToString(), culture); + } +#endif + } +} diff --git a/src/Ookii.CommandLine/Conversion/ParseConverter.cs b/src/Ookii.CommandLine/Conversion/ParseConverter.cs new file mode 100644 index 00000000..eb02d18e --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ParseConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +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) + { + var parameters = _hasCulture + ? new object?[] { value, culture } + : new object?[] { value }; + + try + { + return _method.Invoke(null, parameters); + } + catch (CommandLineArgumentException) + { + throw; + } + catch (FormatException) + { + throw; + } + 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); + } + } + } +} From 01fbe005b7301fa7729be1f06b74f2497c66cc3b Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 29 Mar 2023 15:11:18 -0700 Subject: [PATCH 003/234] Use ReadOnlyMemory to avoid allocating strings for argument names during parsing. --- .../CommandLineParserTest.cs | 2 +- src/Ookii.CommandLine/CommandLineArgument.cs | 18 ++-- src/Ookii.CommandLine/CommandLineParser.cs | 102 ++++++++++++------ src/Ookii.CommandLine/ParseOptions.cs | 18 ++-- .../ParseOptionsAttribute.cs | 10 +- src/Ookii.CommandLine/StringExtensions.cs | 22 +++- src/Ookii.CommandLine/UsageWriter.cs | 11 +- 7 files changed, 125 insertions(+), 58 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 8c5ed908..7d58ed0b 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -611,7 +611,7 @@ public void TestParseOptionsAttribute() var options = new ParseOptions() { Mode = ParsingMode.Default, - ArgumentNameComparer = StringComparer.OrdinalIgnoreCase, + ArgumentNameComparison = StringComparison.OrdinalIgnoreCase, AllowWhiteSpaceValueSeparator = true, DuplicateArguments = ErrorMode.Error, NameValueSeparator = ';', diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 1a255cea..11b8f016 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -262,6 +262,7 @@ private struct MethodArgumentInfo private readonly bool _isHidden; private readonly IEnumerable _validators; private IValueHelper? _valueHelper; + private ReadOnlyMemory _usedArgumentName; private CommandLineArgument(ArgumentInfo info) { @@ -916,7 +917,7 @@ public bool AllowsDuplicateDictionaryKeys /// 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; } + public string? UsedArgumentName => _usedArgumentName.Length > 0 ? _usedArgumentName.ToString() : null; /// /// Gets a value that indicates whether or not this argument accepts values. @@ -1359,18 +1360,18 @@ internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParse Validators = Enumerable.Empty(), }; - var shortNameString = shortName.ToString(); - var shortAliasString = shortAlias.ToString(); if (parser.Mode == ParsingMode.LongShort) { - if (parser.ArgumentNameComparer.Compare(shortAliasString, shortNameString) != 0) + if (parser.ShortArgumentNameComparer!.Compare(shortAlias, shortName) != 0) { info.ShortAliases = new[] { shortAlias }; } } else { - info.Aliases = parser.ArgumentNameComparer.Compare(shortAliasString, shortNameString) == 0 + var shortNameString = shortName.ToString(); + var shortAliasString = shortAlias.ToString(); + info.Aliases = string.Compare(shortAliasString, shortNameString, parser.ArgumentNameComparison) == 0 ? new[] { shortNameString } : new[] { shortNameString, shortAliasString }; } @@ -1449,7 +1450,7 @@ internal void Reset() } HasValue = false; - UsedArgumentName = null; + _usedArgumentName = default; } internal static void ShowVersion(LocalizedStringProvider stringProvider, Assembly assembly, string friendlyName) @@ -1474,6 +1475,11 @@ internal void ValidateAfterParsing() } } + internal void SetUsedArgumentName(ReadOnlyMemory name) + { + _usedArgumentName = name; + } + private static string? GetMultiValueSeparator(MultiValueSeparatorAttribute? attribute) { var separator = attribute?.Separator; diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 01781c01..fc03ddcf 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -84,11 +85,11 @@ public class CommandLineParser private sealed class CommandLineArgumentComparer : IComparer { - private readonly IComparer _stringComparer; + private readonly StringComparison _comparison; - public CommandLineArgumentComparer(IComparer stringComparer) + public CommandLineArgumentComparer(StringComparison comparison) { - _stringComparer = stringComparer; + _comparison = comparison; } public int Compare(CommandLineArgument? x, CommandLineArgument? y) @@ -141,7 +142,41 @@ public int Compare(CommandLineArgument? x, CommandLineArgument? y) } // Sort the rest by name - return _stringComparer.Compare(x.ArgumentName, y.ArgumentName); + 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); + } } } @@ -155,11 +190,8 @@ private struct PrefixInfo 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 SortedDictionary, CommandLineArgument> _argumentsByName; + private readonly SortedDictionary? _argumentsByShortName; private readonly int _positionalArgumentCount; private readonly ParseOptions _parseOptions; @@ -281,7 +313,8 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) } _mode = _parseOptions.Mode ?? default; - var comparer = _parseOptions.ArgumentNameComparer ?? StringComparer.OrdinalIgnoreCase; + var comparison = _parseOptions.ArgumentNameComparison ?? StringComparison.OrdinalIgnoreCase; + ArgumentNameComparison = comparison; _argumentNamePrefixes = DetermineArgumentNamePrefixes(_parseOptions); var prefixInfos = _argumentNamePrefixes.Select(p => new PrefixInfo { Prefix = p, Short = true }); if (_mode == ParsingMode.LongShort) @@ -294,17 +327,17 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) var longInfo = new PrefixInfo { Prefix = _longArgumentNamePrefix, Short = false }; prefixInfos = prefixInfos.Append(longInfo); - _argumentsByShortName = new(comparer); + _argumentsByShortName = new(new CharComparer(comparison)); } _sortedPrefixes = prefixInfos.OrderByDescending(info => info.Prefix.Length).ToArray(); - _argumentsByName = new(comparer); + _argumentsByName = new(new MemoryComparer(comparison)); _positionalArgumentCount = DetermineMemberArguments(options, optionsAttribute); DetermineAutomaticArguments(options, optionsAttribute); // Sort the member arguments in usage order (positional first, then required // non-positional arguments, then the rest by name. - _arguments.Sort(new CommandLineArgumentComparer(_argumentsByName.Comparer)); + _arguments.Sort(new CommandLineArgumentComparer(comparison)); VerifyPositionalArgumentRules(); } @@ -614,11 +647,11 @@ public IEnumerable Validators /// Gets the string comparer used for argument names. /// /// - /// An instance of a class implementing the interface. + /// One of the members of the enumeration. /// /// - /// - public IComparer ArgumentNameComparer => _argumentsByName.Comparer; + /// + public StringComparison ArgumentNameComparison { get; } /// /// Gets the arguments supported by this instance. @@ -658,6 +691,9 @@ public IEnumerable Validators /// public ParseResult ParseResult { get; private set; } + internal IComparer? ShortArgumentNameComparer => _argumentsByShortName?.Comparer; + + /// /// Gets the name of the executable used to invoke the application. /// @@ -1035,7 +1071,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = throw new ArgumentNullException(nameof(name)); } - if (_argumentsByName.TryGetValue(name, out var argument)) + if (_argumentsByName.TryGetValue(name.AsMemory(), out var argument)) { return argument; } @@ -1059,7 +1095,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// public CommandLineArgument? GetShortArgument(char shortName) { - if (_argumentsByShortName != null && _argumentsByShortName.TryGetValue(shortName.ToString(), out var argument)) + if (_argumentsByShortName != null && _argumentsByShortName.TryGetValue(shortName, out var argument)) { return argument; } @@ -1266,24 +1302,24 @@ private void AddNamedArgument(CommandLineArgument argument) if (argument.HasLongName) { - _argumentsByName.Add(argument.ArgumentName, argument); + _argumentsByName.Add(argument.ArgumentName.AsMemory(), argument); if (argument.Aliases != null) { foreach (string alias in argument.Aliases) { - _argumentsByName.Add(alias, argument); + _argumentsByName.Add(alias.AsMemory(), argument); } } } if (_argumentsByShortName != null && argument.HasShortName) { - _argumentsByShortName.Add(argument.ShortName.ToString(), argument); + _argumentsByShortName.Add(argument.ShortName, argument); if (argument.ShortAliases != null) { foreach (var alias in argument.ShortAliases) { - _argumentsByShortName.Add(alias.ToString(), argument); + _argumentsByShortName.Add(alias, argument); } } } @@ -1453,29 +1489,30 @@ private bool ParseArgumentValue(CommandLineArgument argument, string? value) private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) { - var (argumentName, argumentValue) = args[index].SplitOnce(NameValueSeparator, prefix.Prefix.Length); + var (argumentName, argumentValueSpan) = args[index].AsMemory(prefix.Prefix.Length).SplitOnce(NameValueSeparator); + var argumentValue = argumentValueSpan.HasValue ? argumentValueSpan.Value.ToString() : null; CommandLineArgument? argument = null; if (_argumentsByShortName != null && prefix.Short) { if (argumentName.Length == 1) { - argument = GetShortArgumentOrThrow(argumentName); + argument = GetShortArgumentOrThrow(argumentName.Span[0]); } else { // ParseShortArgument returns true if parsing was canceled by the // ArgumentParsed event handler or the CancelParsing property. - return ParseShortArgument(argumentName, argumentValue) ? -1 : index; + return ParseShortArgument(argumentName.Span, argumentValue) ? -1 : index; } } if (argument == null && !_argumentsByName.TryGetValue(argumentName, out argument)) { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, argumentName); + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, argumentName.ToString()); } - argument.UsedArgumentName = argumentName; + argument.SetUsedArgumentName(argumentName); if (argumentValue == null && !argument.IsSwitch && AllowWhiteSpaceValueSeparator) { // No separator was present but a value is required. We take the next argument as @@ -1510,14 +1547,14 @@ private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) return ParseArgumentValue(argument, argumentValue) ? -1 : index; } - private bool ParseShortArgument(string name, string? value) + private bool ParseShortArgument(ReadOnlySpan name, string? value) { foreach (var ch in name) { - var arg = GetShortArgumentOrThrow(ch.ToString()); + var arg = GetShortArgumentOrThrow(ch); if (!arg.IsSwitch) { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, name); + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, name.ToString()); } if (ParseArgumentValue(arg, value)) @@ -1529,15 +1566,14 @@ private bool ParseShortArgument(string name, string? value) return false; } - private CommandLineArgument GetShortArgumentOrThrow(string shortName) + private CommandLineArgument GetShortArgumentOrThrow(char shortName) { - Debug.Assert(shortName.Length == 1); if (_argumentsByShortName!.TryGetValue(shortName, out CommandLineArgument? argument)) { return argument; } - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, shortName); + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, shortName.ToString()); } private PrefixInfo? CheckArgumentNamePrefix(string argument) diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 9f277a8b..bdfa0795 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -137,13 +137,15 @@ public class ParseOptions public string? LongArgumentNamePrefix { get; set; } /// - /// Gets or set the to use to compare argument names. + /// Gets or set the type of string comparison to use for 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 . + /// 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 + /// . /// /// /// @@ -151,8 +153,8 @@ public class ParseOptions /// property. /// /// - /// - public IComparer? ArgumentNameComparer { get; set; } + /// + public StringComparison? ArgumentNameComparison { get; set; } /// /// Gets or sets the used to print error information if argument @@ -522,7 +524,7 @@ public void Merge(ParseOptionsAttribute attribute) ArgumentNameTransform ??= attribute.ArgumentNameTransform; ArgumentNamePrefixes ??= attribute.ArgumentNamePrefixes; LongArgumentNamePrefix ??= attribute.LongArgumentNamePrefix; - ArgumentNameComparer ??= attribute.GetStringComparer(); + ArgumentNameComparison ??= attribute.GetStringComparison(); DuplicateArguments ??= attribute.DuplicateArguments; AllowWhiteSpaceValueSeparator ??= attribute.AllowWhiteSpaceValueSeparator; NameValueSeparator ??= attribute.NameValueSeparator; diff --git a/src/Ookii.CommandLine/ParseOptionsAttribute.cs b/src/Ookii.CommandLine/ParseOptionsAttribute.cs index 7f66f0eb..a373951c 100644 --- a/src/Ookii.CommandLine/ParseOptionsAttribute.cs +++ b/src/Ookii.CommandLine/ParseOptionsAttribute.cs @@ -131,11 +131,11 @@ public class ParseOptionsAttribute : Attribute /// it will use . /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// - /// + /// public bool CaseSensitive { get; set; } /// @@ -306,17 +306,17 @@ public class ParseOptionsAttribute : Attribute /// public NameTransform ValueDescriptionTransform { get; set; } - internal IComparer GetStringComparer() + internal StringComparison GetStringComparison() { if (CaseSensitive) { // Do not use Ordinal for case-sensitive comparisons so that when sorting capitals // and non-capitals are sorted together. - return StringComparer.InvariantCulture; + return StringComparison.InvariantCulture; } else { - return StringComparer.OrdinalIgnoreCase; + return StringComparison.OrdinalIgnoreCase; } } } diff --git a/src/Ookii.CommandLine/StringExtensions.cs b/src/Ookii.CommandLine/StringExtensions.cs index b2cc8ebe..3f23a00b 100644 --- a/src/Ookii.CommandLine/StringExtensions.cs +++ b/src/Ookii.CommandLine/StringExtensions.cs @@ -1,11 +1,13 @@ -namespace Ookii.CommandLine +using System; + +namespace Ookii.CommandLine { internal static class StringExtensions { - public static (string, string?) SplitOnce(this string value, char separator, int start = 0) + public static (ReadOnlyMemory, ReadOnlyMemory?) SplitOnce(this ReadOnlyMemory value, char separator) { - 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 (string, string?) SplitOnce(this string value, string separator, int start = 0) @@ -25,5 +27,17 @@ private static (string, string?) SplitAt(this string value, int index, int start var after = value.Substring(index + skip); return (before, after); } + + private static (ReadOnlyMemory, ReadOnlyMemory?) SplitAt(this ReadOnlyMemory value, int index, int skip) + { + if (index < 0) + { + return (value, null); + } + + var before = value.Slice(0, index); + var after = value.Slice(index + skip); + return (before, after); + } } } diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index cf6a3da7..09073aad 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -1599,7 +1599,16 @@ protected IEnumerable GetFilteredAndSortedArguments() _ => false, }); - var comparer = Parser.ArgumentNameComparer; + var comparer = Parser.ArgumentNameComparison switch + { + StringComparison.CurrentCulture => StringComparer.CurrentCulture, + StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase, + StringComparison.InvariantCulture => StringComparer.InvariantCulture, + StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase, + StringComparison.Ordinal => StringComparer.Ordinal, + _ => StringComparer.OrdinalIgnoreCase, + }; + return ArgumentDescriptionListOrder switch { DescriptionListSortMode.Alphabetical => arguments.OrderBy(arg => arg.ArgumentName, comparer), From fe103a65634ad8ac23a50dceedc929a096bdad3e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 30 Mar 2023 14:54:08 -0700 Subject: [PATCH 004/234] Switch to new argument conversion method. --- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 5 +- .../CommandLineParserNullableTest.cs | 126 +++++++----------- .../CommandLineParserTest.cs | 23 ++-- .../KeyValuePairConverterTest.cs | 20 +-- .../Ookii.CommandLine.Tests.csproj | 2 +- src/Ookii.CommandLine/CommandLineArgument.cs | 53 ++++---- .../ConstructorTypeConverter.cs | 33 ----- .../Conversion/ArgumentConverter.cs | 53 ++++++-- .../Conversion/ArgumentConverterAttribute.cs | 74 ++++++++++ .../Conversion/ConstructorConverter.cs | 36 +++++ .../Conversion/EnumConverter.cs | 48 +++++++ .../Conversion/KeyConverterAttribute.cs | 78 +++++++++++ .../Conversion/KeyValuePairConverter.cs | 104 +++++++++++++++ .../Conversion/KeyValueSeparatorAttribute.cs | 49 +++++++ .../Conversion/NullableConverter.cs | 34 +++++ .../Conversion/ParseConverter.cs | 5 - .../Conversion/SpanParsableConverter.cs | 17 +++ .../Conversion/StringConverter.cs | 15 +++ .../Conversion/ValueConverterAttribute.cs | 78 +++++++++++ .../KeyTypeConverterAttribute.cs | 56 -------- .../KeyValuePairConverter.cs | 119 ----------------- .../KeyValueSeparatorAttribute.cs | 51 ------- .../LocalizedStringProvider.Error.cs | 3 +- .../NullableConverterWrapper.cs | 39 ------ .../Ookii.CommandLine.csproj | 4 +- src/Ookii.CommandLine/ParseTypeConverter.cs | 40 ------ .../Properties/Resources.Designer.cs | 18 +-- .../Properties/Resources.resx | 4 +- src/Ookii.CommandLine/StringExtensions.cs | 20 +-- src/Ookii.CommandLine/TypeConverterBase.cs | 105 --------------- src/Ookii.CommandLine/TypeHelper.cs | 37 +++-- .../Validation/ValidateEnumValueAttribute.cs | 11 +- .../Validation/ValidateNotNullAttribute.cs | 7 +- .../Validation/ValidateRangeAttribute.cs | 3 +- .../ValueTypeConverterAttribute.cs | 53 -------- src/Samples/Parser/ProgramArguments.cs | 5 +- src/Samples/Subcommand/EncodingConverter.cs | 10 +- src/Samples/Subcommand/ReadCommand.cs | 5 +- src/Samples/Subcommand/WriteCommand.cs | 5 +- 39 files changed, 743 insertions(+), 705 deletions(-) delete mode 100644 src/Ookii.CommandLine/ConstructorTypeConverter.cs create mode 100644 src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs create mode 100644 src/Ookii.CommandLine/Conversion/ConstructorConverter.cs create mode 100644 src/Ookii.CommandLine/Conversion/EnumConverter.cs create mode 100644 src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs create mode 100644 src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs create mode 100644 src/Ookii.CommandLine/Conversion/KeyValueSeparatorAttribute.cs create mode 100644 src/Ookii.CommandLine/Conversion/NullableConverter.cs create mode 100644 src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs create mode 100644 src/Ookii.CommandLine/Conversion/StringConverter.cs create mode 100644 src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs delete mode 100644 src/Ookii.CommandLine/KeyTypeConverterAttribute.cs delete mode 100644 src/Ookii.CommandLine/KeyValuePairConverter.cs delete mode 100644 src/Ookii.CommandLine/KeyValueSeparatorAttribute.cs delete mode 100644 src/Ookii.CommandLine/NullableConverterWrapper.cs delete mode 100644 src/Ookii.CommandLine/ParseTypeConverter.cs delete mode 100644 src/Ookii.CommandLine/TypeConverterBase.cs delete mode 100644 src/Ookii.CommandLine/ValueTypeConverterAttribute.cs diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 05715fa0..0cbdb65d 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; @@ -80,7 +81,7 @@ public IDictionary Arg14 get { return _arg14; } } - [CommandLineArgument, TypeConverter(typeof(KeyValuePairConverter))] + [CommandLineArgument, ArgumentConverter(typeof(KeyValuePairConverter))] public KeyValuePair Arg15 { get; set; } public string NotAnArg { get; set; } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs index 2c7b9473..d0660457 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -5,9 +5,9 @@ #nullable enable using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Conversion; using System; using System.Collections.Generic; -using System.ComponentModel; using System.Globalization; namespace Ookii.CommandLine.Tests @@ -17,63 +17,33 @@ public class CommandLineParserNullableTest { #region Nested types - class NullReturningStringConverter : TypeConverter + class NullReturningStringConverter : ArgumentConverter { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + public override object? Convert(string value, CultureInfo culture) { - if (sourceType == typeof(string)) + if (value == "(null)") { - return true; + return null; } - - return base.CanConvertFrom(context, sourceType); - } - - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - { - if (value is string s) + else { - if (s == "(null)") - { - return null; - } - else - { - return s; - } + return value; } - - return base.ConvertFrom(context, culture, value); } } - class NullReturningIntConverter : TypeConverter + class NullReturningIntConverter : ArgumentConverter { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + public override object? Convert(string value, CultureInfo culture) { - if (sourceType == typeof(string)) + if (value == "(null)") { - return true; + return null; } - - return base.CanConvertFrom(context, sourceType); - } - - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - { - if (value is string s) + else { - if (s == "(null)") - { - return null; - } - else - { - return int.Parse(s); - } + return int.Parse(value); } - - return base.ConvertFrom(context, culture, value); } } @@ -81,10 +51,10 @@ class TestArguments { // TODO: Put back with new ctor approach. //public TestArguments( - // [TypeConverter(typeof(NullReturningStringConverter))] string? constructorNullable, - // [TypeConverter(typeof(NullReturningStringConverter))] string constructorNonNullable, - // [TypeConverter(typeof(NullReturningIntConverter))] int constructorValueType, - // [TypeConverter(typeof(NullReturningIntConverter))] int? constructorNullableValueType) + // [ArgumentConverter(typeof(NullReturningStringConverter))] string? constructorNullable, + // [ArgumentConverter(typeof(NullReturningStringConverter))] string constructorNonNullable, + // [ArgumentConverter(typeof(NullReturningIntConverter))] int constructorValueType, + // [ArgumentConverter(typeof(NullReturningIntConverter))] int? constructorNullableValueType) //{ // ConstructorNullable = constructorNullable; // ConstructorNonNullable = constructorNonNullable; @@ -93,114 +63,114 @@ class TestArguments //} [CommandLineArgument("constructorNullable", Position = 0)] - [TypeConverter(typeof(NullReturningStringConverter))] + [ArgumentConverter(typeof(NullReturningStringConverter))] public string? ConstructorNullable { get; set; } [CommandLineArgument("constructorNonNullable", Position = 1)] - [TypeConverter(typeof(NullReturningStringConverter))] + [ArgumentConverter(typeof(NullReturningStringConverter))] public string ConstructorNonNullable { get; set; } = default!; [CommandLineArgument("constructorValueType", Position = 2)] - [TypeConverter(typeof(NullReturningIntConverter))] + [ArgumentConverter(typeof(NullReturningIntConverter))] public int ConstructorValueType { get; set; } [CommandLineArgument("constructorNullableValueType", Position = 3)] - [TypeConverter(typeof(NullReturningIntConverter))] + [ArgumentConverter(typeof(NullReturningIntConverter))] public int? ConstructorNullableValueType { get; set; } [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] + [ArgumentConverter(typeof(NullReturningStringConverter))] public string? Nullable { get; set; } = "NotNullDefaultValue"; [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] + [ArgumentConverter(typeof(NullReturningStringConverter))] public string NonNullable { get; set; } = string.Empty; [CommandLineArgument] - [TypeConverter(typeof(NullReturningIntConverter))] + [ArgumentConverter(typeof(NullReturningIntConverter))] public int ValueType { get; set; } [CommandLineArgument] - [TypeConverter(typeof(NullReturningIntConverter))] + [ArgumentConverter(typeof(NullReturningIntConverter))] public int? NullableValueType { get; set; } = 42; [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] + [ArgumentConverter(typeof(NullReturningStringConverter))] public string[]? NonNullableArray { get; set; } [CommandLineArgument] - [TypeConverter(typeof(NullReturningIntConverter))] + [ArgumentConverter(typeof(NullReturningIntConverter))] public int[]? ValueArray { get; set; } [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] + [ArgumentConverter(typeof(NullReturningStringConverter))] public ICollection NonNullableCollection { get; } = new List(); [CommandLineArgument] - [TypeConverter(typeof(NullReturningIntConverter))] + [ArgumentConverter(typeof(NullReturningIntConverter))] [MultiValueSeparator(";")] public ICollection ValueCollection { get; } = new List(); [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] + [ArgumentConverter(typeof(NullReturningStringConverter))] public string?[]? NullableArray { get; set; } [CommandLineArgument] - [TypeConverter(typeof(NullReturningIntConverter))] + [ArgumentConverter(typeof(NullReturningIntConverter))] public string?[]? NullableValueArray { get; set; } [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] + [ArgumentConverter(typeof(NullReturningStringConverter))] public ICollection NullableCollection { get; } = new List(); [CommandLineArgument] - [TypeConverter(typeof(NullReturningStringConverter))] + [ArgumentConverter(typeof(NullReturningStringConverter))] public ICollection NullableValueCollection { get; } = new List(); [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningStringConverter))] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningStringConverter))] public Dictionary? NonNullableDictionary { get; set; } [CommandLineArgument] - [ValueTypeConverter(typeof(NullReturningIntConverter))] + [ValueConverter(typeof(NullReturningIntConverter))] public Dictionary? ValueDictionary { get; set; } [CommandLineArgument] - [ValueTypeConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningStringConverter))] public IDictionary NonNullableIDictionary { get; } = new Dictionary(); [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningIntConverter))] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningIntConverter))] [MultiValueSeparator(";")] public IDictionary ValueIDictionary { get; } = new Dictionary(); [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningStringConverter))] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningStringConverter))] public Dictionary? NullableDictionary { get; set; } [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningIntConverter))] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningIntConverter))] public Dictionary? NullableValueDictionary { get; set; } [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningStringConverter))] + [KeyConverter(typeof(NullReturningStringConverter))] + [ValueConverter(typeof(NullReturningStringConverter))] public IDictionary NullableIDictionary { get; } = new Dictionary(); [CommandLineArgument] - [KeyTypeConverter(typeof(NullReturningStringConverter))] - [ValueTypeConverter(typeof(NullReturningIntConverter))] + [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] - [TypeConverter(typeof(NullReturningStringConverter))] + [ArgumentConverter(typeof(NullReturningStringConverter))] public Dictionary? InvalidDictionary { get; set; } } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 7d58ed0b..a02b0c9c 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -17,13 +17,6 @@ namespace Ookii.CommandLine.Tests [TestClass()] public partial class CommandLineParserTest { -#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 - /// ///A test for CommandLineParser Constructor /// @@ -273,13 +266,13 @@ public void ParseTestKeyValueSeparator() "CustomSeparator", typeof(FormatException)); - // Inner exception is Argument exception because what throws here is trying to convert + // Inner exception is FormatException because what throws here is trying to convert // ">bar" to int. CheckThrows(() => target.Parse(new[] { "-DefaultSeparator", "foo<=>bar" }), target, CommandLineArgumentErrorCategory.ArgumentValueConversion, "DefaultSeparator", - ArgumentConversionInner); + typeof(FormatException)); } [TestMethod] @@ -639,13 +632,19 @@ public void TestCulture() 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" })); + result = CommandLineParser.Parse(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 = 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)); + result = CommandLineParser.Parse(new[] { "-Argument", "5,5" }); + Assert.IsNotNull(result); + // . was interpreted as a thousands separator. + Assert.AreEqual(55, result.Argument); } [TestMethod] @@ -1031,7 +1030,7 @@ public void TestMultiValueWhiteSpaceSeparator() 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); + CheckThrows(() => parser.Parse(new[] { "-MultiSwitch", "true", "false" }), parser, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Arg1", typeof(FormatException)); parser.Options.AllowWhiteSpaceValueSeparator = false; CheckThrows(() => parser.Parse(new[] { "1", "-Multi:2", "2", "3", "4", "-Other", "5", "6" }), parser, CommandLineArgumentErrorCategory.TooManyArguments); } diff --git a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs index 16755cb7..06be1716 100644 --- a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs +++ b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs @@ -1,5 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Conversion; using System.Collections.Generic; +using System.Globalization; namespace Ookii.CommandLine.Tests { @@ -10,30 +12,16 @@ public class KeyValuePairConverterTest public void TestConvertFrom() { var converter = new KeyValuePairConverter(); - Assert.IsTrue(converter.CanConvertFrom(null, typeof(string))); - var converted = converter.ConvertFromInvariantString(null, "foo=5"); + var converted = converter.Convert("foo=5", CultureInfo.InvariantCulture); 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); - } - [TestMethod] public void TestCustomSeparator() { var converter = new KeyValuePairConverter(new LocalizedStringProvider(), "Test", false, null, null, ":"); - var pair = converter.ConvertFromInvariantString(null, "foo:5"); + var pair = converter.Convert("foo:5", CultureInfo.InvariantCulture); Assert.AreEqual(KeyValuePair.Create("foo", 5), pair); - - Assert.IsTrue(converter.CanConvertTo(null, typeof(string))); - var converted = converter.ConvertToInvariantString(null, KeyValuePair.Create("bar", 6)); - Assert.AreEqual("bar:6", converted); } } } diff --git a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj index 79248260..2d182c52 100644 --- a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj +++ b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj @@ -1,7 +1,7 @@ - net6.0;net48 + net7.0;net6.0;net48 disable Tests for Ookii.CommandLine. false diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 11b8f016..6b2c4767 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1,4 +1,5 @@ // Copyright (c) Sven Groot (Ookii.org) +using Ookii.CommandLine.Conversion; using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; @@ -236,7 +237,7 @@ private struct MethodArgumentInfo #endregion private readonly CommandLineParser _parser; - private readonly TypeConverter _converter; + private readonly ArgumentConverter _converter; private readonly PropertyInfo? _property; private readonly MethodArgumentInfo? _method; private readonly string _valueDescription; @@ -336,7 +337,7 @@ private CommandLineArgument(ArgumentInfo info) if (converterType == null) { converterType = typeof(KeyValuePairConverter<,>).MakeGenericType(genericArguments); - _converter = (TypeConverter)Activator.CreateInstance(converterType, _parser.StringProvider, _argumentName, _allowNull, info.KeyConverterType, info.ValueConverterType, _keyValueSeparator)!; + _converter = (ArgumentConverter)Activator.CreateInstance(converterType, _parser.StringProvider, _argumentName, _allowNull, info.KeyConverterType, info.ValueConverterType, _keyValueSeparator)!; } var valueDescription = info.ValueDescription ?? GetDefaultValueDescription(_elementTypeWithNullable, @@ -941,8 +942,8 @@ public bool AllowsDuplicateDictionaryKeys /// a value type. /// /// - /// This property indicates what happens when the used for this argument returns - /// from its + /// This property indicates what happens when the used for this argument returns + /// from its /// method. /// /// @@ -1042,10 +1043,10 @@ public bool AllowsDuplicateDictionaryKeys /// The converted value. /// /// - /// Conversion is done by one of several methods. First, if a + /// 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 + /// 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. @@ -1078,7 +1079,7 @@ public bool AllowsDuplicateDictionaryKeys try { - var converted = _converter.ConvertFrom(null, culture, argumentValue); + var converted = _converter.Convert(argumentValue, culture); if (converted == null && (!_allowNull || IsDictionary)) { throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, this); @@ -1094,18 +1095,6 @@ public bool AllowsDuplicateDictionaryKeys { 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; - } - } } /// @@ -1114,7 +1103,7 @@ public bool AllowsDuplicateDictionaryKeys /// The value to convert. /// The converted value. /// - /// The argument's cannot convert between the type of + /// The argument's cannot convert between the type of /// and the . /// /// @@ -1123,7 +1112,7 @@ public bool AllowsDuplicateDictionaryKeys /// 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. + /// for the argument is used to convert between the source. /// /// /// This method is used to convert the @@ -1139,7 +1128,13 @@ public bool AllowsDuplicateDictionaryKeys return value; } - return _converter.ConvertFrom(null, CultureInfo.InvariantCulture, value); + var stringValue = value.ToString(); + if (stringValue == null) + { + return null; + } + + return _converter.Convert(stringValue, CultureInfo.InvariantCulture); } /// @@ -1282,9 +1277,9 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo throw new ArgumentException(Properties.Resources.MissingArgumentAttribute, nameof(method)); } - var typeConverterAttribute = member.GetCustomAttribute(); - var keyTypeConverterAttribute = member.GetCustomAttribute(); - var valueTypeConverterAttribute = member.GetCustomAttribute(); + var converterAttribute = member.GetCustomAttribute(); + var keyArgumentConverterAttribute = member.GetCustomAttribute(); + var valueArgumentConverterAttribute = member.GetCustomAttribute(); var multiValueSeparatorAttribute = member.GetCustomAttribute(); var argumentName = DetermineArgumentName(attribute.ArgumentName, member.Name, parser.Options.ArgumentNameTransform); var info = new ArgumentInfo() @@ -1301,9 +1296,9 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo 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), + ConverterType = converterAttribute == null ? null : Type.GetType(converterAttribute.ConverterTypeName, true), + KeyConverterType = keyArgumentConverterAttribute == null ? null : Type.GetType(keyArgumentConverterAttribute.ConverterTypeName, true), + ValueConverterType = valueArgumentConverterAttribute == null ? null : Type.GetType(valueArgumentConverterAttribute.ConverterTypeName, true), MultiValueSeparator = GetMultiValueSeparator(multiValueSeparatorAttribute), AllowMultiValueWhiteSpaceSeparator = multiValueSeparatorAttribute != null && multiValueSeparatorAttribute.Separator == null, KeyValueSeparator = member.GetCustomAttribute()?.Separator, 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 index e4a8c605..c3077dd2 100644 --- a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs @@ -1,17 +1,50 @@ using System; using System.Globalization; -namespace Ookii.CommandLine.Conversion +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 { - public abstract class ArgumentConverter - { - public abstract object? Convert(string value, CultureInfo culture); + /// + /// Convert a string to the type of the argument. + /// + /// The string to convert. + /// The culture to use for the conversion. + /// An object representing the converted value. + /// + /// The value was not in a correct format for the target type. + /// + public abstract object? Convert(string value, CultureInfo culture); -#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - public virtual object? Convert(ReadOnlySpan value, CultureInfo culture) - { - return Convert(value.ToString(), culture); - } -#endif + /// + /// Convert a string to the type of the argument. + /// + /// The containing the string to convert. + /// The culture to use for the conversion. + /// An object representing the converted value. + /// + /// + /// The default implementation of this method will allocate a string and call + /// . Implement this method if it's possible to + /// + /// + /// + /// + /// The value was not in a correct format for the target type. + /// + public virtual object? Convert(ReadOnlySpan value, CultureInfo culture) + { + return Convert(value.ToString(), culture); } } diff --git a/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs new file mode 100644 index 00000000..9f020d0a --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs @@ -0,0 +1,74 @@ +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. +/// +/// +/// Apply this attribute to the property or method defining the 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. + /// + /// + /// 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; } +} diff --git a/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs new file mode 100644 index 00000000..87503379 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +internal class ConstructorConverter : ArgumentConverter +{ + private readonly Type _type; + + public ConstructorConverter(Type type) + { + _type = type; + } + + public override object? Convert(string value, CultureInfo culture) + { + try + { + return _type.CreateInstance(value); + } + catch (CommandLineArgumentException) + { + throw; + } + catch (FormatException) + { + throw; + } + 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/EnumConverter.cs b/src/Ookii.CommandLine/Conversion/EnumConverter.cs new file mode 100644 index 00000000..abf0f289 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/EnumConverter.cs @@ -0,0 +1,48 @@ +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +internal class EnumConverter : ArgumentConverter +{ + private readonly Type _enumType; + + public EnumConverter(Type enumType) + { + _enumType = enumType; + } + + public override object? Convert(string value, CultureInfo culture) + { + try + { + return Enum.Parse(_enumType, value, true); + } + catch (ArgumentException ex) + { + throw new FormatException(ex.Message, ex); + } + catch (OverflowException ex) + { + throw new FormatException(ex.Message, ex); + } + } + +#if NET6_0_OR_GREATER + public override object? Convert(ReadOnlySpan value, CultureInfo culture) + { + try + { + return Enum.Parse(_enumType, value, true); + } + catch (ArgumentException ex) + { + throw new FormatException(ex.Message, ex); + } + catch (OverflowException ex) + { + throw new FormatException(ex.Message, ex); + } + } +#endif +} diff --git a/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs new file mode 100644 index 00000000..16d8ff94 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Specifies a to use for the keys of a dictionary argument. +/// +/// +/// +/// This attribute can be used along with the and +/// attribute 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 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. + /// + /// + /// 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; } +} diff --git a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs new file mode 100644 index 00000000..5f1a3567 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs @@ -0,0 +1,104 @@ +// Copyright (c) Sven Groot (Ookii.org) +using System; +using System.Collections.Generic; +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. +/// +/// +public class KeyValuePairConverter : ArgumentConverter +{ + private readonly ArgumentConverter _keyConverter; + private readonly ArgumentConverter _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. + /// + /// + /// 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. + /// + public KeyValuePairConverter() + : this(new LocalizedStringProvider(), string.Empty, true, null, null, null) + { + } + + /// + public override object? Convert(string value, CultureInfo culture) + => Convert((value ?? throw new ArgumentNullException(nameof(value))).AsSpan(), culture); + + /// + public override object? Convert(ReadOnlySpan value, CultureInfo culture) + { + var (key, valueForKey) = value.SplitOnce(_separator.AsSpan(), out bool hasSeparator); + if (!hasSeparator) + { + throw new FormatException(_stringProvider.MissingKeyValuePairSeparator(_separator)); + } + + object? convertedKey = _keyConverter.Convert(key, culture); + object? convertedValue = _valueConverter.Convert(valueForKey, culture); + if (convertedKey == null || !_allowNullValues && convertedValue == null) + { + throw _stringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, _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..f22db716 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/KeyValueSeparatorAttribute.cs @@ -0,0 +1,49 @@ +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. + /// + 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..7bfbd59e --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/NullableConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +internal class NullableConverter : ArgumentConverter +{ + private readonly ArgumentConverter _baseConverter; + + public NullableConverter(ArgumentConverter baseConverter) + { + _baseConverter = baseConverter; + } + + public override object? Convert(string value, CultureInfo culture) + { + if (value.Length == 0) + { + return null; + } + + return _baseConverter.Convert(value, culture); + } + + public override object? Convert(ReadOnlySpan value, CultureInfo culture) + { + if (value.Length == 0) + { + return null; + } + + return _baseConverter.Convert(value, culture); + } +} diff --git a/src/Ookii.CommandLine/Conversion/ParseConverter.cs b/src/Ookii.CommandLine/Conversion/ParseConverter.cs index eb02d18e..348d3ae6 100644 --- a/src/Ookii.CommandLine/Conversion/ParseConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ParseConverter.cs @@ -1,11 +1,6 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Globalization; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Ookii.CommandLine.Conversion { diff --git a/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs new file mode 100644 index 00000000..61fc90e4 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs @@ -0,0 +1,17 @@ +#if NET7_0_OR_GREATER + +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion +{ + internal class SpanParsableConverter : ArgumentConverter + where T : ISpanParsable + { + public override object? Convert(string value, CultureInfo culture) => T.Parse(value, culture); + + public override object? Convert(ReadOnlySpan value, CultureInfo culture) => 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..9fa3d4f0 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/StringConverter.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine.Conversion; + +internal class StringConverter : ArgumentConverter +{ + public static readonly StringConverter Instance = new(); + + public override object? Convert(string value, CultureInfo culture) => value; +} diff --git a/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs new file mode 100644 index 00000000..5f32fdd2 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Ookii.CommandLine.Conversion; + +/// +/// Specifies a to use for the keys of a dictionary argument. +/// +/// +/// +/// This attribute can be used along with the and +/// attribute 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. + /// + /// + /// 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; } +} 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/LocalizedStringProvider.Error.cs b/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs index 644cb88e..b995ecfa 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs @@ -1,4 +1,5 @@ -using Ookii.CommandLine.Properties; +using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Properties; using System; using System.Diagnostics; 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..537e24da 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -1,9 +1,9 @@  - net6.0;netstandard2.0;netstandard2.1 + net7.0;net6.0;netstandard2.0;netstandard2.1 enable - 9.0 + 11.0 True True MIT 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/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 81e1566a..2d6edf11 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -429,6 +429,15 @@ internal static string MultipleMarkedConstructors { } } + /// + /// Looks up a localized string similar to No argument converter exists for type '{0}'. Use the ArgumentConverterAttribute to specify a custom ArgumentConverter.. + /// + internal static string NoArgumentConverterFormat { + get { + return ResourceManager.GetString("NoArgumentConverterFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to The command line arguments type does not have any public constructors.. /// @@ -474,15 +483,6 @@ 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.. - /// - internal static string NoTypeConverterFormat { - get { - return ResourceManager.GetString("NoTypeConverterFormat", resourceCulture); - } - } - /// /// Looks up a localized string similar to The argument '{0}' cannot be null.. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index a7005e97..46f4c726 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -204,8 +204,8 @@ 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. + + No argument converter exists for type '{0}'. Use the ArgumentConverterAttribute to specify a custom ArgumentConverter. An error occurred creating an instance of the arguments type: {0} diff --git a/src/Ookii.CommandLine/StringExtensions.cs b/src/Ookii.CommandLine/StringExtensions.cs index 3f23a00b..767f5d40 100644 --- a/src/Ookii.CommandLine/StringExtensions.cs +++ b/src/Ookii.CommandLine/StringExtensions.cs @@ -10,34 +10,36 @@ public static (ReadOnlyMemory, ReadOnlyMemory?) SplitOnce(this ReadO 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, start, separator.Length); + return value.SplitAt(index, separator.Length, out hasSeparator); } - private static (string, string?) SplitAt(this string value, int index, int start, int skip) + private static (ReadOnlyMemory, ReadOnlyMemory?) SplitAt(this ReadOnlyMemory value, int index, int skip) { if (index < 0) { - return (value.Substring(start), null); + return (value, null); } - var before = value.Substring(start, index - start); - var after = value.Substring(index + skip); + var before = value.Slice(0, index); + var after = value.Slice(index + skip); return (before, after); } - private static (ReadOnlyMemory, ReadOnlyMemory?) SplitAt(this ReadOnlyMemory value, int index, int skip) + private static StringSpanTuple SplitAt(this ReadOnlySpan value, int index, int skip, out bool hasSeparator) { if (index < 0) { - return (value, null); + hasSeparator = false; + return new(value, default); } var before = value.Slice(0, index); var after = value.Slice(index + skip); - return (before, after); + hasSeparator = true; + return new(before, after); } } } 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..de4c1262 100644 --- a/src/Ookii.CommandLine/TypeHelper.cs +++ b/src/Ookii.CommandLine/TypeHelper.cs @@ -1,6 +1,6 @@ // Copyright (c) Sven Groot (Ookii.org) +using Ookii.CommandLine.Conversion; using System; -using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; @@ -71,15 +71,15 @@ public static bool ImplementsInterface(this Type type, Type interfaceType) return Activator.CreateInstance(type, args); } - public static TypeConverter GetStringConverter(this Type type, Type? converterType) + public static ArgumentConverter GetStringConverter(this Type type, Type? converterType) { if (type == null) { throw new ArgumentNullException(nameof(type)); } - var converter = (TypeConverter?)converterType?.CreateInstance() ?? TypeDescriptor.GetConverter(type); - if (converter != null && converter.CanConvertFrom(typeof(string))) + var converter = (ArgumentConverter?)converterType?.CreateInstance(); + if (converter != null) { return converter; } @@ -91,12 +91,12 @@ public static TypeConverter GetStringConverter(this Type type, Type? converterTy if (converter != null) { return type.IsNullableValueType() - ? new NullableConverterWrapper(underlyingType, converter) + ? new NullableConverter(converter) : converter; } } - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoTypeConverterFormat, type)); + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoArgumentConverterFormat, type)); } public static bool IsNullableValueType(this Type type) @@ -107,8 +107,25 @@ public static bool IsNullableValueType(this Type type) public static Type GetUnderlyingType(this Type type) => type.IsNullableValueType() ? type.GetGenericArguments()[0] : type; - private static TypeConverter? GetDefaultConverter(this Type type) + private static ArgumentConverter? GetDefaultConverter(this Type type) { + if (type == typeof(string)) + { + return StringConverter.Instance; + } + + if (type.IsEnum) + { + return new EnumConverter(type); + } + +#if NET7_0_OR_GREATER + if (type.FindGenericInterface(typeof(ISpanParsable<>)) != null) + { + return (ArgumentConverter?)Activator.CreateInstance(typeof(SpanParsableConverter<>).MakeGenericType(type)); + } +#endif + // 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, @@ -116,7 +133,7 @@ public static Type GetUnderlyingType(this Type type) if (method != null && method.ReturnType == type) { - return new ParseTypeConverter(method, true); + return new ParseConverter(method, true); } // Check for Parse without a culture arguments. @@ -125,13 +142,13 @@ public static Type GetUnderlyingType(this Type type) if (method != null && method.ReturnType == type) { - return new ParseTypeConverter(method, false); + return new ParseConverter(method, false); } // Check for a constructor with a string argument. if (type.GetConstructor(new[] { typeof(string) }) != null) { - return new ConstructorTypeConverter(type); + return new ConstructorConverter(type); } return null; diff --git a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs index 690c9c71..65226c94 100644 --- a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs @@ -1,6 +1,6 @@ using System; -using System.ComponentModel; using System.Globalization; +using Ookii.CommandLine.Conversion; namespace Ookii.CommandLine.Validation { @@ -10,11 +10,10 @@ namespace Ookii.CommandLine.Validation /// /// /// - /// 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. + /// 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 diff --git a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs index 6a0ceff1..718d22fe 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs @@ -1,4 +1,5 @@ -using System; +using Ookii.CommandLine.Conversion; +using System; using System.ComponentModel; namespace Ookii.CommandLine.Validation @@ -8,8 +9,8 @@ namespace Ookii.CommandLine.Validation /// /// /// - /// An argument's value can only be if its - /// returns from the + /// An argument's value can only be if its + /// returns from the /// method. For example, the can return . /// /// diff --git a/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs index 58235bb0..33b375c9 100644 --- a/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs @@ -31,8 +31,7 @@ public class ValidateRangeAttribute : ArgumentValidationWithHelpAttribute /// /// /// 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 . + /// must be an instance of the argument type, or a string. /// /// /// 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/Samples/Parser/ProgramArguments.cs b/src/Samples/Parser/ProgramArguments.cs index 9e97ed2f..4b0519ff 100644 --- a/src/Samples/Parser/ProgramArguments.cs +++ b/src/Samples/Parser/ProgramArguments.cs @@ -46,9 +46,8 @@ class ProgramArguments // 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. + // 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(Position = 2, DefaultValue = 1)] [Description("The operation's index.")] public int OperationIndex { get; set; } diff --git a/src/Samples/Subcommand/EncodingConverter.cs b/src/Samples/Subcommand/EncodingConverter.cs index 066553ff..e7b7f041 100644 --- a/src/Samples/Subcommand/EncodingConverter.cs +++ b/src/Samples/Subcommand/EncodingConverter.cs @@ -1,4 +1,4 @@ -using Ookii.CommandLine; +using Ookii.CommandLine.Conversion; using System; using System.ComponentModel; using System.Globalization; @@ -6,11 +6,11 @@ namespace SubcommandSample; -// A TypeConverter for the Encoding class, using the utility base class provided by +// A ArgumentConverter for the Encoding class, using the utility base class provided by // Ookii.CommandLine. -internal class EncodingConverter : TypeConverterBase +internal class EncodingConverter : ArgumentConverter { - protected override Encoding? Convert(ITypeDescriptorContext? context, CultureInfo? culture, string value) + public override object? Convert(string value, CultureInfo culture) { try { @@ -18,7 +18,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/ReadCommand.cs b/src/Samples/Subcommand/ReadCommand.cs index 5140d889..9618be57 100644 --- a/src/Samples/Subcommand/ReadCommand.cs +++ b/src/Samples/Subcommand/ReadCommand.cs @@ -1,5 +1,6 @@ using Ookii.CommandLine; using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Conversion; using System; using System.ComponentModel; using System.IO; @@ -37,11 +38,11 @@ public ReadCommand([Description("The name of the file to read.")] FileInfo path) } // 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] [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. diff --git a/src/Samples/Subcommand/WriteCommand.cs b/src/Samples/Subcommand/WriteCommand.cs index 37332e58..8f349e7e 100644 --- a/src/Samples/Subcommand/WriteCommand.cs +++ b/src/Samples/Subcommand/WriteCommand.cs @@ -1,5 +1,6 @@ using Ookii.CommandLine; using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Conversion; using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; @@ -45,11 +46,11 @@ public WriteCommand([Description("The name of the file to write to.")] FileInfo 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] [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. From f9bd8994664deaba91400a29949941a524d908fd Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 30 Mar 2023 15:41:19 -0700 Subject: [PATCH 005/234] Avoid creating strings for argument values if possible. --- src/Ookii.CommandLine/CommandLineArgument.cs | 133 ++++++++++-------- src/Ookii.CommandLine/CommandLineParser.cs | 30 ++-- .../DuplicateArgumentEventArgs.cs | 25 +++- .../Ookii.CommandLine.csproj | 4 +- src/Ookii.CommandLine/StringSpanExtensions.cs | 15 ++ 5 files changed, 132 insertions(+), 75 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 6b2c4767..ded34d1a 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1043,59 +1043,24 @@ public bool AllowsDuplicateDictionaryKeys /// 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. + /// 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 , 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. + /// 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)); - } - - if (argumentValue == null) - { - if (IsSwitch) - { - return true; - } - else - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.MissingNamedArgumentValue, this); - } - } - - try - { - var converted = _converter.Convert(argumentValue, culture); - if (converted == null && (!_allowNull || IsDictionary)) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, this); - } - - 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); - } - } + => ConvertToArgumentType(culture, argumentValue != null, argumentValue, argumentValue.AsSpan()); /// /// Converts any type to the argument's . @@ -1111,8 +1076,8 @@ public bool AllowsDuplicateDictionaryKeys /// 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. + /// method, using . Other types cannot be + /// converted. /// /// /// This method is used to convert the @@ -1152,6 +1117,48 @@ public override string ToString() return (new UsageWriter()).GetArgumentUsage(this); } + internal object? ConvertToArgumentType(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) + { + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); + } + + if (!hasValue) + { + if (IsSwitch) + { + return true; + } + else + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.MissingNamedArgumentValue, this); + } + } + + try + { + var converted = stringValue == null + ? _converter.Convert(spanValue, culture) + : _converter.Convert(stringValue, culture); + + if (converted == null && (!_allowNull || IsDictionary)) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, this); + } + + return converted; + } + catch (NotSupportedException ex) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); + } + catch (FormatException ex) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); + } + } + internal bool HasInformation(UsageWriter writer) { if (!string.IsNullOrEmpty(Description)) @@ -1196,32 +1203,33 @@ internal bool HasInformation(UsageWriter writer) return false; } - internal bool SetValue(CultureInfo culture, string? value) + internal bool SetValue(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) { _valueHelper ??= CreateValueHelper(); bool continueParsing; - if (IsMultiValue && value != null && MultiValueSeparator != null) + if (IsMultiValue && hasValue && MultiValueSeparator != null) { continueParsing = true; - string[] values = value.Split(new[] { MultiValueSeparator }, StringSplitOptions.None); - foreach (string separateValue in values) + spanValue.Split(MultiValueSeparator.AsSpan(), separateValue => { - Validate(separateValue, ValidationMode.BeforeConversion); - var converted = ConvertToArgumentType(culture, separateValue); + string? separateValueString = null; + PreValidate(ref separateValueString, separateValue); + var converted = ConvertToArgumentType(culture, true, separateValueString, separateValue); continueParsing = _valueHelper.SetValue(this, culture, converted); if (!continueParsing) { - break; + return false; } Validate(converted, ValidationMode.AfterConversion); - } + return true; + }); } else { - Validate(value, ValidationMode.BeforeConversion); - var converted = ConvertToArgumentType(culture, value); + PreValidate(ref stringValue, spanValue); + var converted = ConvertToArgumentType(culture, hasValue, stringValue, spanValue); continueParsing = _valueHelper.SetValue(this, culture, converted); Validate(converted, ValidationMode.AfterConversion); } @@ -1838,6 +1846,17 @@ private void Validate(object? value, ValidationMode mode) } } + private void PreValidate(ref string? stringValue, ReadOnlySpan spanValue) + { + foreach (var validator in _validators) + { + if (validator.Mode == ValidationMode.BeforeConversion) + { + validator.Validate(this, (stringValue ??= spanValue.ToString())); + } + } + } + private static string? GetDefaultValueDescription(Type type, IDictionary? defaultValueDescriptions) { if (defaultValueDescriptions == null) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index fc03ddcf..77affb7c 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1414,7 +1414,7 @@ private void VerifyPositionalArgumentRules() // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler // or the CancelParsing property. - if (ParseArgumentValue(_arguments[positionalArgumentIndex], arg)) + if (ParseArgumentValue(_arguments[positionalArgumentIndex], arg, arg.AsMemory())) { return null; } @@ -1446,7 +1446,7 @@ private void VerifyPositionalArgumentRules() return commandLineArguments; } - private bool ParseArgumentValue(CommandLineArgument argument, string? value) + private bool ParseArgumentValue(CommandLineArgument argument, string? stringValue, ReadOnlyMemory? memoryValue) { if (argument.HasValue && !argument.IsMultiValue) { @@ -1455,7 +1455,10 @@ private bool ParseArgumentValue(CommandLineArgument argument, string? value) throw StringProvider.CreateException(CommandLineArgumentErrorCategory.DuplicateArgument, argument); } - var duplicateEventArgs = new DuplicateArgumentEventArgs(argument, value); + var duplicateEventArgs = stringValue == null + ? new DuplicateArgumentEventArgs(argument, memoryValue.HasValue, memoryValue ?? default) + : new DuplicateArgumentEventArgs(argument, stringValue); + OnDuplicateArgument(duplicateEventArgs); if (duplicateEventArgs.KeepOldValue) { @@ -1463,7 +1466,7 @@ private bool ParseArgumentValue(CommandLineArgument argument, string? value) } } - bool continueParsing = argument.SetValue(Culture, value); + bool continueParsing = argument.SetValue(Culture, memoryValue.HasValue, stringValue, (memoryValue ?? default).Span); var e = new ArgumentParsedEventArgs(argument) { Cancel = !continueParsing @@ -1489,8 +1492,7 @@ private bool ParseArgumentValue(CommandLineArgument argument, string? value) private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) { - var (argumentName, argumentValueSpan) = args[index].AsMemory(prefix.Prefix.Length).SplitOnce(NameValueSeparator); - var argumentValue = argumentValueSpan.HasValue ? argumentValueSpan.Value.ToString() : null; + var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitOnce(NameValueSeparator); CommandLineArgument? argument = null; if (_argumentsByShortName != null && prefix.Short) @@ -1513,19 +1515,21 @@ private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) } argument.SetUsedArgumentName(argumentName); - if (argumentValue == null && !argument.IsSwitch && AllowWhiteSpaceValueSeparator) + if (!argumentValue.HasValue && !argument.IsSwitch && AllowWhiteSpaceValueSeparator) { + 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) { ++index; - argumentValue = args[index]; + argumentValueString = args[index]; // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed // event handler or the CancelParsing property. - if (ParseArgumentValue(argument, argumentValue)) + if (ParseArgumentValue(argument, argumentValueString, argumentValueString.AsMemory())) { return -1; } @@ -1536,7 +1540,7 @@ private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) } } - if (argumentValue != null) + if (argumentValueString != null) { return index; } @@ -1544,10 +1548,10 @@ private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler // or the CancelParsing property. - return ParseArgumentValue(argument, argumentValue) ? -1 : index; + return ParseArgumentValue(argument, null, argumentValue) ? -1 : index; } - private bool ParseShortArgument(ReadOnlySpan name, string? value) + private bool ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? value) { foreach (var ch in name) { @@ -1557,7 +1561,7 @@ private bool ParseShortArgument(ReadOnlySpan name, string? value) throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, name.ToString()); } - if (ParseArgumentValue(arg, value)) + if (ParseArgumentValue(arg, null, value)) { return true; } diff --git a/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs b/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs index a3ce0438..3a3929a0 100644 --- a/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs +++ b/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs @@ -8,7 +8,9 @@ namespace Ookii.CommandLine public class DuplicateArgumentEventArgs : EventArgs { private readonly CommandLineArgument _argument; - private readonly string? _newValue; + private readonly ReadOnlyMemory _memoryValue; + private readonly string? _stringValue; + private bool _hasValue; /// /// Initializes a new instance of the class. @@ -21,7 +23,24 @@ public class DuplicateArgumentEventArgs : EventArgs public DuplicateArgumentEventArgs(CommandLineArgument argument, string? newValue) { _argument = argument ?? throw new ArgumentNullException(nameof(argument)); - _newValue = newValue; + _stringValue = newValue; + _hasValue = newValue != null; + } + + /// + /// Initializes a new instance of the class. + /// + /// The argument that was specified more than once. + /// if the argument has a value; otherwise, . + /// The 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; } /// @@ -38,7 +57,7 @@ public DuplicateArgumentEventArgs(CommandLineArgument argument, string? newValue /// /// The raw string value provided on the command line, before conversion. /// - public string? NewValue => _newValue; + public string? NewValue => _hasValue ? (_stringValue ?? _memoryValue.ToString()) : null; /// /// Gets or sets a value that indicates whether the value of the argument should stay diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index 537e24da..828845dc 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -29,8 +29,8 @@ true - - CS1574 + + CS1574 diff --git a/src/Ookii.CommandLine/StringSpanExtensions.cs b/src/Ookii.CommandLine/StringSpanExtensions.cs index 676fc2fe..0d57fcb8 100644 --- a/src/Ookii.CommandLine/StringSpanExtensions.cs +++ b/src/Ookii.CommandLine/StringSpanExtensions.cs @@ -12,6 +12,7 @@ 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' }; @@ -75,6 +76,20 @@ public static void CopyTo(this ReadOnlySpan self, char[] destination, int self.CopyTo(destination.AsSpan(start)); } + public static void Split(this ReadOnlySpan self, ReadOnlySpan separator, SplitCallback callback) + { + while (!self.IsEmpty) + { + var (first, remaining) = self.SplitOnce(separator, out bool _); + if (!callback(first)) + { + break; + } + + self = remaining; + } + } + public static partial void WriteTo(this ReadOnlySpan self, TextWriter writer); private static (int, int)? BreakLine(ReadOnlySpan span, int startIndex, BreakLineMode mode) From 90d6acf5f5cb8bbe3466da9d4828bd6ad764f29d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 30 Mar 2023 16:15:49 -0700 Subject: [PATCH 006/234] Enable validators to use ReadOnlySpan. --- src/Ookii.CommandLine/CommandLineArgument.cs | 15 +++- .../Validation/ArgumentValidationAttribute.cs | 78 ++++++++++++++++++- .../Validation/ValidateNotEmptyAttribute.cs | 25 ++++-- .../ValidateNotWhiteSpaceAttribute.cs | 19 ++++- .../Validation/ValidatePatternAttribute.cs | 26 ++++++- .../ValidateStringLengthAttribute.cs | 15 +++- 6 files changed, 160 insertions(+), 18 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index ded34d1a..f96be527 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1852,7 +1852,20 @@ private void PreValidate(ref string? stringValue, ReadOnlySpan spanValue) { if (validator.Mode == ValidationMode.BeforeConversion) { - validator.Validate(this, (stringValue ??= spanValue.ToString())); + if (stringValue == null) + { + if (validator.CanValidateSpan) + { + validator.ValidateSpan(this, spanValue); + continue; + } + else + { + stringValue = spanValue.ToString(); + } + } + + validator.Validate(this, stringValue); } } } diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs index abebfdea..9ae96486 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs @@ -47,6 +47,16 @@ public abstract class ArgumentValidationAttribute : Attribute /// public virtual CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.ValidationFailed; + /// + /// Gets a value that indicates whether this instance can validate a + /// when using . + /// + /// + /// if the validator implements , otherwise, + /// . The default value is . + /// + public bool CanValidateSpan { get; protected set; } + /// /// Validates the argument value, and throws an exception if validation failed. /// @@ -73,19 +83,58 @@ public void Validate(CommandLineArgument argument, object? value) } /// - /// When overridden in a derived class, determines if the argument is valid. + /// 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 class will only call this method if the + /// property is , and the + /// property is . + /// + /// + /// + /// The parameter is not a valid value. The + /// property will be the value of the property. + /// + public void ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) + { + if (argument == null) + { + throw new ArgumentNullException(nameof(argument)); + } + + if (!IsSpanValid(argument, value)) + { + 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 a string or an + /// instance of . + /// /// /// if the value is valid; otherwise, . /// /// /// - /// For regular arguments, the parameter will be identical to + /// 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 equal the last value added /// to the collection or dictionary. @@ -103,6 +152,31 @@ public void Validate(CommandLineArgument argument, object? value) /// 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 the value is valid; otherwise, . + /// + /// + /// + /// The class will only call this method if the + /// property is , and 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. + /// + /// + public virtual bool IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + => throw new NotImplementedException(); + /// /// Gets the error message to display if validation failed. /// diff --git a/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs index 3b6bf2e7..e10716cc 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs @@ -1,12 +1,14 @@ -namespace Ookii.CommandLine.Validation +using System; + +namespace Ookii.CommandLine.Validation { /// /// Validates that the value of an argument is not an empty string. /// /// /// - /// 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 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, @@ -17,6 +19,14 @@ /// public class ValidateNotEmptyAttribute : ArgumentValidationWithHelpAttribute { + /// + /// Initializes a new instance of the class. + /// + public ValidateNotEmptyAttribute() + { + CanValidateSpan = true; + } + /// /// Gets a value that indicates when validation will run. /// @@ -26,12 +36,11 @@ public class ValidateNotEmptyAttribute : ArgumentValidationWithHelpAttribute public override ValidationMode Mode => ValidationMode.BeforeConversion; /// - /// Determines if the argument's value is not null or empty. + /// Determines if the argument is valid. /// /// The argument being validated. /// - /// The argument value. If not , this must be an instance of - /// . + /// The raw string argument value provided by the user on the command line. /// /// /// if the value is valid; otherwise, . @@ -41,6 +50,10 @@ public override bool IsValid(CommandLineArgument argument, object? value) return !string.IsNullOrEmpty(value as string); } + /// + public override bool IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + => !value.IsEmpty; + /// /// Gets the error message to display if validation failed. /// diff --git a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs index 9c78bc1b..62bbcf21 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs @@ -1,4 +1,6 @@ -namespace Ookii.CommandLine.Validation +using System; + +namespace Ookii.CommandLine.Validation { /// @@ -19,6 +21,14 @@ /// public class ValidateNotWhiteSpaceAttribute : ArgumentValidationWithHelpAttribute { + /// + /// Initializes a new instance of the class. + /// + public ValidateNotWhiteSpaceAttribute() + { + CanValidateSpan = true; + } + /// /// Gets a value that indicates when validation will run. /// @@ -32,8 +42,7 @@ public class ValidateNotWhiteSpaceAttribute : ArgumentValidationWithHelpAttribut /// /// The argument being validated. /// - /// The argument value. If not , this must be an instance of - /// . + /// The raw string argument value. /// /// /// if the value is valid; otherwise, . @@ -43,6 +52,10 @@ 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. /// diff --git a/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs b/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs index e77886c6..1f23b8d0 100644 --- a/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System; +using System.Globalization; using System.Text.RegularExpressions; namespace Ookii.CommandLine.Validation @@ -38,6 +39,9 @@ public ValidatePatternAttribute(string pattern, RegexOptions options = RegexOpti { _pattern = pattern; _options = options; +#if NET7_0_OR_GREATER + CanValidateSpan = true; +#endif } /// @@ -81,8 +85,7 @@ public ValidatePatternAttribute(string pattern, RegexOptions options = RegexOpti /// /// The argument being validated. /// - /// The argument value. If not , this must be an instance of - /// . + /// The raw string argument value. /// /// /// if the value is valid; otherwise, . @@ -97,6 +100,23 @@ public override bool IsValid(CommandLineArgument argument, object? value) return Pattern.IsMatch(stringValue); } +#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. /// diff --git a/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs index e95c96af..49ee9a4f 100644 --- a/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs @@ -1,4 +1,6 @@ -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. @@ -24,6 +26,7 @@ public ValidateStringLengthAttribute(int minimum, int maximum = int.MaxValue) { _minimum = minimum; _maximum = maximum; + CanValidateSpan = true; } /// @@ -55,8 +58,7 @@ public ValidateStringLengthAttribute(int minimum, int maximum = int.MaxValue) /// /// The argument being validated. /// - /// The argument value. If not , this must be an instance of - /// . + /// The raw string value of the argument. /// /// /// if the value is valid; otherwise, . @@ -67,6 +69,13 @@ public override bool IsValid(CommandLineArgument argument, object? value) 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. /// From 0055a31efa1754ecf16ba8c142e0b61c14dfe782 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 30 Mar 2023 18:12:09 -0700 Subject: [PATCH 007/234] WIP: Separate out reflection stuff in CommandLineArgument. --- src/Ookii.CommandLine/CommandLineArgument.cs | 482 +----------------- .../Conversion/ArgumentConverterAttribute.cs | 5 + .../Conversion/KeyConverterAttribute.cs | 5 + .../Conversion/ValueConverterAttribute.cs | 5 + src/Ookii.CommandLine/ReflectionArgument.cs | 452 ++++++++++++++++ 5 files changed, 477 insertions(+), 472 deletions(-) create mode 100644 src/Ookii.CommandLine/ReflectionArgument.cs diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index f96be527..4905cb0e 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -18,7 +18,7 @@ namespace Ookii.CommandLine /// /// /// - public sealed class CommandLineArgument + public class CommandLineArgument { #region Nested types @@ -195,12 +195,9 @@ public bool SetValue(CommandLineArgument argument, CultureInfo culture, object? } } - private struct ArgumentInfo + internal 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; } @@ -209,9 +206,10 @@ private struct ArgumentInfo 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 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 object? DefaultValue { get; set; } @@ -227,19 +225,10 @@ private struct ArgumentInfo public IEnumerable Validators { get; set; } } - private struct MethodArgumentInfo - { - public MethodInfo Method { get; set; } - public bool HasValueParameter { get; set; } - public bool HasParserParameter { get; set; } - } - #endregion private readonly CommandLineParser _parser; private readonly ArgumentConverter _converter; - private readonly PropertyInfo? _property; - private readonly MethodArgumentInfo? _method; private readonly string _valueDescription; private readonly string _argumentName; private readonly bool _hasLongName = true; @@ -265,12 +254,10 @@ private struct MethodArgumentInfo private IValueHelper? _valueHelper; private ReadOnlyMemory _usedArgumentName; - private CommandLineArgument(ArgumentInfo info) + 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; - _property = info.Property; - _method = info.Method; _memberName = info.MemberName; _argumentName = info.ArgumentName; if (_parser.Mode == ParsingMode.LongShort) @@ -319,72 +306,6 @@ private CommandLineArgument(ArgumentInfo info) // 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 = (ArgumentConverter)Activator.CreateInstance(converterType, _parser.StringProvider, _argumentName, _allowNull, info.KeyConverterType, info.ValueConverterType, _keyValueSeparator)!; - } - - var valueDescription = info.ValueDescription ?? GetDefaultValueDescription(_elementTypeWithNullable, - _parser.Options.DefaultValueDescriptions); - - if (valueDescription == null) - { - var key = DetermineValueDescription(genericArguments[0].GetUnderlyingType(), _parser.Options); - var value = DetermineValueDescription(genericArguments[1].GetUnderlyingType(), _parser.Options); - valueDescription = $"{key}{_keyValueSeparator}{value}"; - } - - _valueDescription = valueDescription; - } - else if (collectionType != null) - { - Debug.Assert(elementType != null); - _argumentKind = ArgumentKind.MultiValue; - _elementTypeWithNullable = elementType!; - _allowNull = DetermineCollectionElementTypeAllowsNull(collectionType, info.Property, info.Parameter); - } - } - 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); - } - _defaultValue = ConvertToArgumentTypeInvariant(info.DefaultValue); } @@ -1238,92 +1159,6 @@ internal bool SetValue(CultureInfo culture, bool hasValue, string? stringValue, return continueParsing; } - 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 infoTuple = DetermineMethodArgumentInfo(method); - if (infoTuple == null) - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.InvalidMethodSignatureFormat, method.Name)); - } - - var (methodInfo, argumentType, allowsNull) = infoTuple.Value; - 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(); - if (attribute == null) - { - throw new ArgumentException(Properties.Resources.MissingArgumentAttribute, nameof(method)); - } - - var converterAttribute = member.GetCustomAttribute(); - var keyArgumentConverterAttribute = member.GetCustomAttribute(); - var valueArgumentConverterAttribute = 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 = converterAttribute == null ? null : Type.GetType(converterAttribute.ConverterTypeName, true), - KeyConverterType = keyArgumentConverterAttribute == null ? null : Type.GetType(keyArgumentConverterAttribute.ConverterTypeName, true), - ValueConverterType = valueArgumentConverterAttribute == null ? null : Type.GetType(valueArgumentConverterAttribute.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(), - }; - - return new CommandLineArgument(info); - } - internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParser parser, IDictionary? defaultValueDescriptions, NameTransform valueDescriptionTransform) { if (parser == null) @@ -1483,52 +1318,6 @@ internal void SetUsedArgumentName(ReadOnlyMemory name) _usedArgumentName = name; } - private static string? GetMultiValueSeparator(MultiValueSeparatorAttribute? attribute) - { - var separator = attribute?.Separator; - if (string.IsNullOrEmpty(separator)) - { - return null; - } - else - { - return separator; - } - } - - 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('<'); - // 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)); - } - - name.Append('>'); - return name.ToString(); - } - else - { - return type.Name; - } - } - private IValueHelper CreateValueHelper() { Debug.Assert(_valueHelper == null); @@ -1552,7 +1341,7 @@ private IValueHelper CreateValueHelper() } } - private static IEnumerable? GetAliases(IEnumerable aliasAttributes, string argumentName) + internal static IEnumerable? GetAliases(IEnumerable aliasAttributes, string argumentName) { if (!aliasAttributes.Any()) { @@ -1570,7 +1359,7 @@ private IValueHelper CreateValueHelper() }); } - private static IEnumerable? GetShortAliases(IEnumerable aliasAttributes, string argumentName) + internal static IEnumerable? GetShortAliases(IEnumerable aliasAttributes, string argumentName) { if (!aliasAttributes.Any()) { @@ -1588,230 +1377,6 @@ private IValueHelper CreateValueHelper() }); } - private static bool DetermineDictionaryValueTypeAllowsNull(Type type, PropertyInfo? property, ParameterInfo? parameter) - { - 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 ?? parameter?.ParameterType; - - // 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!); - } - - return info.GenericTypeArguments[1].ReadState != NullabilityState.NotNull; - } -#endif - - return true; - } - - 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; - } - -#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; - - // 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!); - } - - if (actualType.IsArray) - { - return info.ElementType?.ReadState != NullabilityState.NotNull; - } - else - { - return info.GenericTypeArguments[0].ReadState != NullabilityState.NotNull; - } - } -#endif - - return true; - } - - 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 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? DetermineValueTypeNullable(Type type) - { - if (type.IsValueType) - { - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - } - - return null; - } - - // Returns a tuple of (collectionType, dictionaryType, elementType) - private (Type?, Type?, Type?) DetermineMultiValueType() - { - // 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 != null && _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 == null || _property.GetSetMethod() != null) - { - return (null, null, null); - } - - var dictionaryType = TypeHelper.FindGenericInterface(_argumentType, typeof(IDictionary<,>)); - if (dictionaryType != null) - { - var elementType = typeof(KeyValuePair<,>).MakeGenericType(dictionaryType.GetGenericArguments()); - return (null, dictionaryType, elementType); - } - - var collectionType = TypeHelper.FindGenericInterface(_argumentType, 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 (MethodArgumentInfo, Type, bool)? DetermineMethodArgumentInfo(MethodInfo method) - { - var parameters = method.GetParameters(); - if (!method.IsStatic || - (method.ReturnType != typeof(bool) && method.ReturnType != typeof(void)) || - 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); - } - private static void AutomaticHelp() { // Intentionally blank. @@ -1825,7 +1390,7 @@ private static bool AutomaticVersion(CommandLineParser parser) return false; } - private static string DetermineArgumentName(string? explicitName, string memberName, NameTransform? transform) + internal static string DetermineArgumentName(string? explicitName, string memberName, NameTransform? transform) { if (explicitName != null) { @@ -1869,32 +1434,5 @@ private void PreValidate(ref string? stringValue, ReadOnlySpan spanValue) } } } - - private static string? GetDefaultValueDescription(Type type, IDictionary? defaultValueDescriptions) - { - if (defaultValueDescriptions == null) - { - return null; - } - - if (defaultValueDescriptions.TryGetValue(type, out string? value)) - { - return value; - } - - 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; - } } } diff --git a/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs index 9f020d0a..3f849c30 100644 --- a/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs @@ -71,4 +71,9 @@ public ArgumentConverterAttribute(string converterTypeName) [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif public string ConverterTypeName { get; } + + internal Type GetConverterType() + { + return Type.GetType(ConverterTypeName, true)!; + } } diff --git a/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs index 16d8ff94..501c1ca9 100644 --- a/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs +++ b/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs @@ -75,4 +75,9 @@ public KeyConverterAttribute(string converterTypeName) [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif public string ConverterTypeName { get; } + + internal Type GetConverterType() + { + return Type.GetType(ConverterTypeName, true)!; + } } diff --git a/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs index 5f32fdd2..b5dfc21f 100644 --- a/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs +++ b/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs @@ -75,4 +75,9 @@ public ValueConverterAttribute(string converterTypeName) [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif public string ConverterTypeName { get; } + + internal Type GetConverterType() + { + return Type.GetType(ConverterTypeName, true)!; + } } diff --git a/src/Ookii.CommandLine/ReflectionArgument.cs b/src/Ookii.CommandLine/ReflectionArgument.cs new file mode 100644 index 00000000..f96e835d --- /dev/null +++ b/src/Ookii.CommandLine/ReflectionArgument.cs @@ -0,0 +1,452 @@ +using System.Collections.Generic; +using System.Globalization; +using System; +using System.Reflection; +using System.ComponentModel; +using Ookii.CommandLine.Conversion; +using System.Diagnostics; +using System.Text; +using Ookii.CommandLine.Validation; + +namespace Ookii.CommandLine; + +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; + } + + 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 infoTuple = DetermineMethodArgumentInfo(method); + if (infoTuple == null) + { + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.InvalidMethodSignatureFormat, method.Name)); + } + + var (methodInfo, argumentType, allowsNull) = infoTuple.Value; + 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)); + + var argumentName = DetermineArgumentName(attribute.ArgumentName, member.Name, parser.Options.ArgumentNameTransform); + var multiValueSeparatorAttribute = member.GetCustomAttribute(); + + var info = new ArgumentInfo() + { + Parser = parser, + 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)), + 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(), + }; + + DetermineAdditionalInfo(ref info, member, argumentType, argumentName); + return new ReflectionArgument(info, property, method); + } + + private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo member, Type argumentType, string argumentName) + { + 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(argumentName, argumentType, property); + + if (dictionaryType != null) + { + Debug.Assert(elementType != null); + info.Kind = ArgumentKind.Dictionary; + info.ElementTypeWithNullable = elementType!; + info.AllowNull = DetermineDictionaryValueTypeAllowsNull(dictionaryType, property); + info.KeyValueSeparator ??= KeyValuePairConverter.DefaultSeparator; + var genericArguments = dictionaryType.GetGenericArguments(); + if (converterType == null) + { + converterType = typeof(KeyValuePairConverter<,>).MakeGenericType(genericArguments); + var keyConverterType = keyArgumentConverterAttribute?.GetConverterType(); + var valueConverterType = valueArgumentConverterAttribute?.GetConverterType(); + info.Converter = (ArgumentConverter)Activator.CreateInstance(converterType, info.Parser.StringProvider, + info.ArgumentName, info.AllowNull, keyConverterType, valueConverterType, info.KeyValueSeparator)!; + } + + var valueDescription = info.ValueDescription ?? GetDefaultValueDescription(info.ElementTypeWithNullable, + info.Parser.Options.DefaultValueDescriptions); + + if (valueDescription == null) + { + var key = DetermineValueDescription(genericArguments[0].GetUnderlyingType(), info.Parser.Options); + var value = DetermineValueDescription(genericArguments[1].GetUnderlyingType(), info.Parser.Options); + valueDescription = $"{key}{info.KeyValueSeparator}{value}"; + } + + info.ValueDescription = valueDescription; + } + else if (collectionType != null) + { + Debug.Assert(elementType != null); + info.Kind = ArgumentKind.MultiValue; + 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); + info.ValueDescription ??= info.ValueDescription ?? DetermineValueDescription(info.ElementType, info.Parser.Options); + } + + private static string? GetDefaultValueDescription(Type type, IDictionary? defaultValueDescriptions) + { + if (defaultValueDescriptions == null) + { + return null; + } + + if (defaultValueDescriptions.TryGetValue(type, out string? value)) + { + return value; + } + + 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; + } + + 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) + { + first = false; + } + else + { + name.Append(", "); + } + + name.Append(GetFriendlyTypeName(typeArgument)); + } + + name.Append('>'); + return name.ToString(); + } + else + { + return type.Name; + } + } + + // 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 = TypeHelper.FindGenericInterface(argumentType, typeof(IDictionary<,>)); + if (dictionaryType != null) + { + var elementType = typeof(KeyValuePair<,>).MakeGenericType(dictionaryType.GetGenericArguments()); + return (null, dictionaryType, elementType); + } + + var collectionType = TypeHelper.FindGenericInterface(argumentType, 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)) || + 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); + } + + private static string? GetMultiValueSeparator(MultiValueSeparatorAttribute? attribute) + { + var separator = attribute?.Separator; + if (string.IsNullOrEmpty(separator)) + { + return null; + } + else + { + return separator; + } + } +} From 0ae5c1cdd730cff3bc30bed19cb6697088611c05 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 31 Mar 2023 14:51:05 -0700 Subject: [PATCH 008/234] Split out reflection from CommandLineArgument. --- src/Ookii.CommandLine/CommandLineArgument.cs | 367 ++++++++++++------ src/Ookii.CommandLine/CommandLineParser.cs | 28 +- .../Conversion/BooleanConverter.cs | 16 + .../Conversion/StringConverter.cs | 1 + src/Ookii.CommandLine/ReflectionArgument.cs | 147 +++---- src/Ookii.CommandLine/TypeHelper.cs | 5 + 6 files changed, 341 insertions(+), 223 deletions(-) create mode 100644 src/Ookii.CommandLine/Conversion/BooleanConverter.cs diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 4905cb0e..93a17f9f 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.Linq; @@ -18,15 +17,15 @@ namespace Ookii.CommandLine /// /// /// - public class CommandLineArgument + public abstract class CommandLineArgument { #region Nested types private interface IValueHelper { object? Value { get; } - bool SetValue(CommandLineArgument argument, CultureInfo culture, object? value); - void ApplyValue(object target, PropertyInfo property); + bool SetValue(CommandLineArgument argument, object? value); + void ApplyValue(CommandLineArgument argument, object target); } private class SingleValueHelper : IValueHelper @@ -38,12 +37,12 @@ public SingleValueHelper(object? initialValue) public object? Value { get; private set; } - public void ApplyValue(object target, PropertyInfo property) + public void ApplyValue(CommandLineArgument argument, object target) { - property.SetValue(target, Value); + argument.SetProperty(target, Value); } - public bool SetValue(CommandLineArgument argument, CultureInfo culture, object? value) + public bool SetValue(CommandLineArgument argument, object? value) { Value = value; return true; @@ -58,21 +57,15 @@ private class MultiValueHelper : IValueHelper public object? Value => _values.ToArray(); - public void ApplyValue(object target, PropertyInfo property) + public void ApplyValue(CommandLineArgument argument, object target) { - if (property.PropertyType.IsArray) + if (argument.CanSetProperty) { - property.SetValue(target, Value); + argument.SetProperty(target, Value); return; } - object? collection = property.GetValue(target, null); - if (collection == null) - { - throw new InvalidOperationException(); - } - - var list = (ICollection)collection; + var list = (ICollection?)argument.GetProperty(target) ?? throw new InvalidOperationException(); list.Clear(); foreach (var value in _values) { @@ -80,7 +73,7 @@ public void ApplyValue(object target, PropertyInfo property) } } - public bool SetValue(CommandLineArgument argument, CultureInfo culture, object? value) + public bool SetValue(CommandLineArgument argument, object? value) { _values.Add((T?)value); return true; @@ -103,19 +96,16 @@ public DictionaryValueHelper(bool allowDuplicateKeys, bool allowNullValues) public object? Value => _dictionary; - public void ApplyValue(object target, PropertyInfo property) + public void ApplyValue(CommandLineArgument argument, object target) { - if (property.GetSetMethod() != null) + if (argument.CanSetProperty) { - property.SetValue(target, _dictionary); + argument.SetProperty(target, _dictionary); return; } - var dictionary = (IDictionary?)property.GetValue(target, null); - if (dictionary == null) - { - throw new InvalidOperationException(); - } + var dictionary = (IDictionary?)argument.GetProperty(target) + ?? throw new InvalidOperationException(); dictionary.Clear(); foreach (var pair in _dictionary) @@ -124,7 +114,7 @@ public void ApplyValue(object target, PropertyInfo property) } } - public bool SetValue(CommandLineArgument argument, CultureInfo culture, object? value) + public bool SetValue(CommandLineArgument argument, object? value) { // ConvertToArgumentType is guaranteed to return non-null for dictionary arguments. var pair = (KeyValuePair)value!; @@ -160,39 +150,101 @@ private class MethodValueHelper : IValueHelper { public object? Value { get; private set; } - public void ApplyValue(object target, PropertyInfo property) + public void ApplyValue(CommandLineArgument argument, object target) { throw new InvalidOperationException(); } - public bool SetValue(CommandLineArgument argument, CultureInfo culture, object? value) + public bool SetValue(CommandLineArgument argument, object? value) { 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; - } + return argument.CallMethod(value); + } + } - if (info.HasParserParameter) - { - parameters[index] = argument._parser; - } + private class HelpArgument : CommandLineArgument + { + public HelpArgument(CommandLineParser parser, string argumentName, char shortName, char shortAlias) + : base(CreateInfo(parser, argumentName, shortName, shortAlias)) + { + } + + protected override bool CanSetProperty => false; - var returnValue = info.Method.Invoke(null, parameters); - if (returnValue == null) + private static ArgumentInfo CreateInfo(CommandLineParser parser, string argumentName, char shortName, char shortAlias) + { + var info = new ArgumentInfo() { - return true; + 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 = true, + Validators = Enumerable.Empty(), + Converter = BooleanConverter.Instance, + }; + + if (parser.Mode == ParsingMode.LongShort) + { + if (parser.ShortArgumentNameComparer!.Compare(shortAlias, shortName) != 0) + { + info.ShortAliases = new[] { shortAlias }; + } } else { - return (bool)returnValue; + var shortNameString = shortName.ToString(); + var shortAliasString = shortAlias.ToString(); + info.Aliases = string.Compare(shortAliasString, shortNameString, parser.ArgumentNameComparison) == 0 + ? new[] { shortNameString } + : new[] { shortNameString, shortAliasString }; } + + return info; } + + protected override bool CallMethod(object? value) => true; + protected override object? GetProperty(object target) => throw new InvalidOperationException(); + protected override void SetProperty(object target, object? value) => throw new InvalidOperationException(); + } + + private class VersionArgument : CommandLineArgument + { + public VersionArgument(CommandLineParser parser, string argumentName) + : base(CreateInfo(parser, argumentName)) + { + } + + protected override bool CanSetProperty => false; + + 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 = BooleanConverter.Instance + }; + } + + protected override bool CallMethod(object? value) => AutomaticVersion(Parser); + protected override object? GetProperty(object target) => throw new InvalidOperationException(); + protected override void SetProperty(object target, object? value) => throw new InvalidOperationException(); } internal struct ArgumentInfo @@ -222,6 +274,8 @@ internal struct ArgumentInfo public bool AllowNull { get; set; } public bool CancelParsing { get; set; } public bool IsHidden { get; set; } + public Type? KeyType { get; set; } + public Type? ValueType { get; set; } public IEnumerable Validators { get; set; } } @@ -229,7 +283,6 @@ internal struct ArgumentInfo private readonly CommandLineParser _parser; private readonly ArgumentConverter _converter; - private readonly string _valueDescription; private readonly string _argumentName; private readonly bool _hasLongName = true; private readonly char _shortName; @@ -238,6 +291,8 @@ internal struct ArgumentInfo private readonly Type _argumentType; private readonly Type _elementType; private readonly Type _elementTypeWithNullable; + private readonly Type? _keyType; + private readonly Type? _valueType; private readonly string? _description; private readonly bool _isRequired; private readonly string _memberName; @@ -251,6 +306,7 @@ internal struct ArgumentInfo private readonly bool _cancelParsing; private readonly bool _isHidden; private readonly IEnumerable _validators; + private string? _valueDescription; private IValueHelper? _valueHelper; private ReadOnlyMemory _usedArgumentName; @@ -297,7 +353,11 @@ internal CommandLineArgument(ArgumentInfo info) } _argumentType = info.ArgumentType; - _elementTypeWithNullable = info.ArgumentType; + _argumentKind = info.Kind; + _elementTypeWithNullable = info.ElementTypeWithNullable; + _elementType = info.ElementType; + _keyType = info.KeyType; + _valueType = info.ValueType; _description = info.Description; _isRequired = info.IsRequired; _allowNull = info.AllowNull; @@ -306,7 +366,14 @@ internal CommandLineArgument(ArgumentInfo info) // 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); + _valueDescription = info.ValueDescription; + _allowDuplicateDictionaryKeys = info.AllowDuplicateDictionaryKeys; + _allowMultiValueWhiteSpaceSeparator = IsMultiValue && !IsSwitch && info.AllowMultiValueWhiteSpaceSeparator; + _allowNull = info.AllowNull; + _keyValueSeparator = info.KeyValueSeparator; + _multiValueSeparator = info.MultiValueSeparator; } /// @@ -619,10 +686,7 @@ public string Description /// /// /// - public string ValueDescription - { - get { return _valueDescription; } - } + public string ValueDescription => _valueDescription ??= DetermineValueDescription(); /// /// Gets a value indicating whether this argument is a switch argument. @@ -917,7 +981,8 @@ public bool AllowsDuplicateDictionaryKeys /// /// /// Canceling parsing in this way is identical to handling the - /// event and setting to . + /// event and setting to + /// . /// /// /// It's possible to prevent cancellation when an argument has this property set by @@ -956,6 +1021,16 @@ public bool AllowsDuplicateDictionaryKeys /// 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; } + /// /// Converts the specified string to the . /// @@ -1038,6 +1113,102 @@ 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); + + /// + /// 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 bool CallMethod(object? value); + + /// + /// Determines the value description if one wasn't explicitly given. + /// + /// + /// The type to get the description for, or null to use the value of the + /// property. + /// + /// The value description. + /// + /// + /// This method is responsible for applying the , + /// if one is specified. + /// + /// + protected virtual string DetermineValueDescription(Type? type = null) + { + if (Kind == ArgumentKind.Dictionary && type == null) + { + var key = DetermineValueDescription(_keyType!.GetUnderlyingType()); + var value = DetermineValueDescription(_valueType!.GetUnderlyingType()); + return $"{key}{KeyValueSeparator}{value}"; + } + + var result = GetDefaultValueDescription(type); + if (result != null) + { + return result; + } + + var typeName = GetFriendlyTypeName(type ?? ElementType); + return Parser.Options.ValueDescriptionTransform?.Apply(typeName) ?? typeName; + } + + 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) + { + first = false; + } + else + { + name.Append(", "); + } + + name.Append(GetFriendlyTypeName(typeArgument)); + } + + name.Append('>'); + return name.ToString(); + } + else + { + return type.Name; + } + } + internal object? ConvertToArgumentType(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) { if (culture == null) @@ -1137,7 +1308,7 @@ internal bool SetValue(CultureInfo culture, bool hasValue, string? stringValue, string? separateValueString = null; PreValidate(ref separateValueString, separateValue); var converted = ConvertToArgumentType(culture, true, separateValueString, separateValue); - continueParsing = _valueHelper.SetValue(this, culture, converted); + continueParsing = _valueHelper.SetValue(this, converted); if (!continueParsing) { return false; @@ -1151,7 +1322,7 @@ internal bool SetValue(CultureInfo culture, bool hasValue, string? stringValue, { PreValidate(ref stringValue, spanValue); var converted = ConvertToArgumentType(culture, hasValue, stringValue, spanValue); - continueParsing = _valueHelper.SetValue(this, culture, converted); + continueParsing = _valueHelper.SetValue(this, converted); Validate(converted, ValidationMode.AfterConversion); } @@ -1159,7 +1330,7 @@ internal bool SetValue(CultureInfo culture, bool hasValue, string? stringValue, return continueParsing; } - internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParser parser, IDictionary? defaultValueDescriptions, NameTransform valueDescriptionTransform) + internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParser parser) { if (parser == null) { @@ -1179,45 +1350,10 @@ internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParse return (existingArg, false); } - 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(), - }; - - if (parser.Mode == ParsingMode.LongShort) - { - if (parser.ShortArgumentNameComparer!.Compare(shortAlias, shortName) != 0) - { - info.ShortAliases = new[] { shortAlias }; - } - } - else - { - var shortNameString = shortName.ToString(); - var shortAliasString = shortAlias.ToString(); - info.Aliases = string.Compare(shortAliasString, shortNameString, parser.ArgumentNameComparison) == 0 - ? new[] { shortNameString } - : new[] { shortNameString, shortAliasString }; - } - - return (new CommandLineArgument(info), true); + return (new HelpArgument(parser, argumentName, shortName, shortAlias), true); } - internal static CommandLineArgument? CreateAutomaticVersion(CommandLineParser parser, IDictionary? defaultValueDescriptions, NameTransform valueDescriptionTransform) + internal static CommandLineArgument? CreateAutomaticVersion(CommandLineParser parser) { if (parser == null) { @@ -1230,24 +1366,7 @@ internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParse return null; } - 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(), - }; - - return new CommandLineArgument(info); + return new VersionArgument(parser, argumentName); } internal object? GetConstructorParameterValue() @@ -1257,18 +1376,16 @@ internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParse internal void ApplyPropertyValue(object target) { - // Do nothing for parameter-based values - if (_property == null) + // Do nothing for method-based values. + // TODO: Handle new style constructor parameters. + if (Kind == ArgumentKind.Method) { return; } try { - if (_valueHelper != null) - { - _valueHelper.ApplyValue(target, _property); - } + _valueHelper?.ApplyValue(this, target); } catch (TargetInvocationException ex) { @@ -1377,11 +1494,6 @@ private IValueHelper CreateValueHelper() }); } - private static void AutomaticHelp() - { - // Intentionally blank. - } - private static bool AutomaticVersion(CommandLineParser parser) { ShowVersion(parser.StringProvider, parser.ArgumentsType.Assembly, parser.ApplicationFriendlyName); @@ -1400,6 +1512,17 @@ internal static string DetermineArgumentName(string? explicitName, string member return transform?.Apply(memberName) ?? memberName; } + private string? GetDefaultValueDescription(Type? type) + { + if (Parser.Options.DefaultValueDescriptions == null || + !Parser.Options.DefaultValueDescriptions.TryGetValue(type ?? ElementType, out string? value)) + { + return null; + } + + return value; + } + private void Validate(object? value, ValidationMode mode) { foreach (var validator in _validators) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 77affb7c..c3f5622d 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -333,8 +333,8 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) _sortedPrefixes = prefixInfos.OrderByDescending(info => info.Prefix.Length).ToArray(); _argumentsByName = new(new MemoryComparer(comparison)); - _positionalArgumentCount = DetermineMemberArguments(options, optionsAttribute); - DetermineAutomaticArguments(options, optionsAttribute); + _positionalArgumentCount = DetermineMemberArguments(); + DetermineAutomaticArguments(); // Sort the member arguments in usage order (positional first, then required // non-positional arguments, then the rest by name. _arguments.Sort(new CommandLineArgumentComparer(comparison)); @@ -1231,11 +1231,8 @@ private static string[] DetermineArgumentNamePrefixes(ParseOptions options) } } - private int DetermineMemberArguments(ParseOptions? options, ParseOptionsAttribute? optionsAttribute) + private int DetermineMemberArguments() { - 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); @@ -1245,8 +1242,8 @@ private int DetermineMemberArguments(ParseOptions? options, ParseOptionsAttribut { var argument = member switch { - PropertyInfo prop => CommandLineArgument.Create(this, prop), - MethodInfo method => CommandLineArgument.Create(this, method), + PropertyInfo prop => ReflectionArgument.Create(this, prop), + MethodInfo method => ReflectionArgument.Create(this, method), _ => throw new InvalidOperationException(), }; @@ -1261,16 +1258,12 @@ private int DetermineMemberArguments(ParseOptions? options, ParseOptionsAttribut return additionalPositionalArgumentCount; } - private void DetermineAutomaticArguments(ParseOptions? options, ParseOptionsAttribute? optionsAttribute) + private void DetermineAutomaticArguments() { - var valueDescriptionTransform = options?.ValueDescriptionTransform ?? optionsAttribute?.ValueDescriptionTransform - ?? NameTransform.None; - - bool autoHelp = options?.AutoHelpArgument ?? optionsAttribute?.AutoHelpArgument ?? true; + bool autoHelp = Options.AutoHelpArgument ?? true; if (autoHelp) { - var (argument, created) = CommandLineArgument.CreateAutomaticHelp(this, options?.DefaultValueDescriptions, - valueDescriptionTransform); + var (argument, created) = CommandLineArgument.CreateAutomaticHelp(this); if (created) { @@ -1280,11 +1273,10 @@ private void DetermineAutomaticArguments(ParseOptions? options, ParseOptionsAttr HelpArgument = argument; } - bool autoVersion = options?.AutoVersionArgument ?? optionsAttribute?.AutoVersionArgument ?? true; + bool autoVersion = Options.AutoVersionArgument ?? true; if (autoVersion && !CommandInfo.IsCommand(_argumentsType)) { - var argument = CommandLineArgument.CreateAutomaticVersion(this, options?.DefaultValueDescriptions, - valueDescriptionTransform); + var argument = CommandLineArgument.CreateAutomaticVersion(this); if (argument != null) { diff --git a/src/Ookii.CommandLine/Conversion/BooleanConverter.cs b/src/Ookii.CommandLine/Conversion/BooleanConverter.cs new file mode 100644 index 00000000..7313cf5e --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/BooleanConverter.cs @@ -0,0 +1,16 @@ +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +// Boolean doesn't support ISpanParsable, so special-case it. +internal class BooleanConverter : ArgumentConverter +{ + public static readonly BooleanConverter Instance = new(); + + public override object? Convert(string value, CultureInfo culture) => bool.Parse(value); + +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + public override object? Convert(ReadOnlySpan value, CultureInfo culture) => bool.Parse(value); +#endif +} diff --git a/src/Ookii.CommandLine/Conversion/StringConverter.cs b/src/Ookii.CommandLine/Conversion/StringConverter.cs index 9fa3d4f0..d2aa6b64 100644 --- a/src/Ookii.CommandLine/Conversion/StringConverter.cs +++ b/src/Ookii.CommandLine/Conversion/StringConverter.cs @@ -7,6 +7,7 @@ namespace Ookii.CommandLine.Conversion; +// Identity converter for strings. internal class StringConverter : ArgumentConverter { public static readonly StringConverter Instance = new(); diff --git a/src/Ookii.CommandLine/ReflectionArgument.cs b/src/Ookii.CommandLine/ReflectionArgument.cs index f96e835d..8bbfe954 100644 --- a/src/Ookii.CommandLine/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/ReflectionArgument.cs @@ -33,6 +33,60 @@ private ReflectionArgument(ArgumentInfo info, PropertyInfo? property, MethodArgu _method = method; } + protected override bool CanSetProperty => _property?.GetSetMethod() != null; + + protected override void SetProperty(object target, object? value) + { + if (_property == null) + { + throw new InvalidOperationException(); + } + + _property.SetValue(target, value); + } + + protected override object? GetProperty(object target) + { + if (_property == null) + { + throw new InvalidOperationException(); + } + + return _property.GetValue(target); + } + + protected override bool CallMethod(object? value) + { + if (_method is not MethodArgumentInfo info) + { + throw new InvalidOperationException(); + } + + 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; + } + + var returnValue = info.Method.Invoke(null, parameters); + if (returnValue == null) + { + return true; + } + else + { + return (bool)returnValue; + } + } + internal static CommandLineArgument Create(CommandLineParser parser, PropertyInfo property) { if (parser == null) @@ -60,13 +114,9 @@ internal static CommandLineArgument Create(CommandLineParser parser, MethodInfo throw new ArgumentNullException(nameof(method)); } - var infoTuple = DetermineMethodArgumentInfo(method); - if (infoTuple == null) - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.InvalidMethodSignatureFormat, method.Name)); - } + var (methodInfo, argumentType, allowsNull) = DetermineMethodArgumentInfo(method) + ?? throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.InvalidMethodSignatureFormat, method.Name)); - var (methodInfo, argumentType, allowsNull) = infoTuple.Value; return Create(parser, null, methodInfo, argumentType, allowsNull); } @@ -88,8 +138,9 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo Short = attribute.IsShort, ShortName = attribute.ShortName, ArgumentType = argumentType, + ElementTypeWithNullable = argumentType, Description = member.GetCustomAttribute()?.Description, - ValueDescription = attribute.ValueDescription, // If null, the constructor will sort it out. + ValueDescription = attribute.ValueDescription, Position = attribute.Position < 0 ? null : attribute.Position, AllowDuplicateDictionaryKeys = Attribute.IsDefined(member, typeof(AllowDuplicateDictionaryKeysAttribute)), MultiValueSeparator = GetMultiValueSeparator(multiValueSeparatorAttribute), @@ -106,11 +157,11 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo Validators = member.GetCustomAttributes(), }; - DetermineAdditionalInfo(ref info, member, argumentType, argumentName); + DetermineAdditionalInfo(ref info, member); return new ReflectionArgument(info, property, method); } - private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo member, Type argumentType, string argumentName) + private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo member) { var converterAttribute = member.GetCustomAttribute(); var keyArgumentConverterAttribute = member.GetCustomAttribute(); @@ -119,7 +170,8 @@ private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo me if (member is PropertyInfo property) { - var (collectionType, dictionaryType, elementType) = DetermineMultiValueType(argumentName, argumentType, property); + var (collectionType, dictionaryType, elementType) = + DetermineMultiValueType(info.ArgumentName, info.ArgumentType, property); if (dictionaryType != null) { @@ -129,6 +181,8 @@ private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo me info.AllowNull = DetermineDictionaryValueTypeAllowsNull(dictionaryType, property); info.KeyValueSeparator ??= KeyValuePairConverter.DefaultSeparator; var genericArguments = dictionaryType.GetGenericArguments(); + info.KeyType = genericArguments[0]; + info.ValueType = genericArguments[1]; if (converterType == null) { converterType = typeof(KeyValuePairConverter<,>).MakeGenericType(genericArguments); @@ -137,18 +191,6 @@ private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo me info.Converter = (ArgumentConverter)Activator.CreateInstance(converterType, info.Parser.StringProvider, info.ArgumentName, info.AllowNull, keyConverterType, valueConverterType, info.KeyValueSeparator)!; } - - var valueDescription = info.ValueDescription ?? GetDefaultValueDescription(info.ElementTypeWithNullable, - info.Parser.Options.DefaultValueDescriptions); - - if (valueDescription == null) - { - var key = DetermineValueDescription(genericArguments[0].GetUnderlyingType(), info.Parser.Options); - var value = DetermineValueDescription(genericArguments[1].GetUnderlyingType(), info.Parser.Options); - valueDescription = $"{key}{info.KeyValueSeparator}{value}"; - } - - info.ValueDescription = valueDescription; } else if (collectionType != null) { @@ -168,67 +210,6 @@ private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo me // Use the original Nullable for this if it is one. info.Converter ??= info.ElementTypeWithNullable.GetStringConverter(converterType); - info.ValueDescription ??= info.ValueDescription ?? DetermineValueDescription(info.ElementType, info.Parser.Options); - } - - private static string? GetDefaultValueDescription(Type type, IDictionary? defaultValueDescriptions) - { - if (defaultValueDescriptions == null) - { - return null; - } - - if (defaultValueDescriptions.TryGetValue(type, out string? value)) - { - return value; - } - - 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; - } - - 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) - { - first = false; - } - else - { - name.Append(", "); - } - - name.Append(GetFriendlyTypeName(typeArgument)); - } - - name.Append('>'); - return name.ToString(); - } - else - { - return type.Name; - } } // Returns a tuple of (collectionType, dictionaryType, elementType) diff --git a/src/Ookii.CommandLine/TypeHelper.cs b/src/Ookii.CommandLine/TypeHelper.cs index de4c1262..d363c806 100644 --- a/src/Ookii.CommandLine/TypeHelper.cs +++ b/src/Ookii.CommandLine/TypeHelper.cs @@ -114,6 +114,11 @@ public static Type GetUnderlyingType(this Type type) return StringConverter.Instance; } + if (type == typeof(bool)) + { + return BooleanConverter.Instance; + } + if (type.IsEnum) { return new EnumConverter(type); From cbd2aeb40999587910b9bb7858b1d1e34ec62d2d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Sat, 1 Apr 2023 16:40:46 -0700 Subject: [PATCH 009/234] Split reflection out of CommandLineParser. --- src/Ookii.CommandLine/CommandLineParser.cs | 114 ++++++++---------- .../Support/IArgumentProvider.cs | 21 ++++ .../Support/ReflectionArgumentProvider.cs | 78 ++++++++++++ 3 files changed, 152 insertions(+), 61 deletions(-) create mode 100644 src/Ookii.CommandLine/Support/IArgumentProvider.cs create mode 100644 src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index c3f5622d..ce6c2e34 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1,5 +1,6 @@ // Copyright (c) Sven Groot (Ookii.org) using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Support; using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; @@ -188,7 +189,7 @@ private struct PrefixInfo #endregion - private readonly Type _argumentsType; + private readonly IArgumentProvider _provider; private readonly List _arguments = new(); private readonly SortedDictionary, CommandLineArgument> _argumentsByName; private readonly SortedDictionary? _argumentsByShortName; @@ -302,11 +303,50 @@ private struct PrefixInfo /// /// public CommandLineParser(Type argumentsType, ParseOptions? options = null) + : this(new ReflectionArgumentProvider(argumentsType ?? throw new ArgumentNullException(nameof(argumentsType))), options) { - _argumentsType = argumentsType ?? throw new ArgumentNullException(nameof(argumentsType)); + } + + /// + /// 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(IArgumentProvider provider, ParseOptions? options = null) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); _parseOptions = options ?? new(); - var optionsAttribute = _argumentsType.GetCustomAttribute(); + var optionsAttribute = _provider.OptionsAttribute; if (optionsAttribute != null) { _parseOptions.Merge(optionsAttribute); @@ -398,10 +438,7 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) /// /// The that was used to define the arguments. /// - public Type ArgumentsType - { - get { return _argumentsType; } - } + public Type ArgumentsType => _provider.ArgumentsType; /// /// Gets the friendly name of the application. @@ -420,16 +457,7 @@ public Type ArgumentsType /// attribute. /// /// - public string ApplicationFriendlyName - { - get - { - var attribute = _argumentsType.GetCustomAttribute() ?? - _argumentsType.Assembly.GetCustomAttribute(); - - return attribute?.Name ?? _argumentsType.Assembly.GetName().Name ?? string.Empty; - } - } + public string ApplicationFriendlyName => _provider.ApplicationFriendlyName; /// /// Gets a description that is used when generating usage information. @@ -444,8 +472,7 @@ public string ApplicationFriendlyName /// to the command line arguments type. /// /// - public string Description - => _argumentsType.GetCustomAttribute()?.Description ?? string.Empty; + public string Description => _provider.Description; /// /// Gets the options used by this instance. @@ -1234,24 +1261,12 @@ private static string[] DetermineArgumentNamePrefixes(ParseOptions options) private int DetermineMemberArguments() { 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)) + foreach (var argument in _provider.GetArguments(this)) { - if (Attribute.IsDefined(member, typeof(CommandLineArgumentAttribute))) + AddNamedArgument(argument); + if (argument.Position != null) { - var argument = member switch - { - PropertyInfo prop => ReflectionArgument.Create(this, prop), - MethodInfo method => ReflectionArgument.Create(this, method), - _ => throw new InvalidOperationException(), - }; - - AddNamedArgument(argument); - if (argument.Position != null) - { - ++additionalPositionalArgumentCount; - } + ++additionalPositionalArgumentCount; } } @@ -1274,7 +1289,7 @@ private void DetermineAutomaticArguments() } bool autoVersion = Options.AutoVersionArgument ?? true; - if (autoVersion && !CommandInfo.IsCommand(_argumentsType)) + if (autoVersion && !CommandInfo.IsCommand(_provider.ArgumentsType)) { var argument = CommandLineArgument.CreateAutomaticVersion(this); @@ -1420,14 +1435,10 @@ private void VerifyPositionalArgumentRules() } // Run class validators. - foreach (var validator in _argumentsType.GetCustomAttributes()) - { - validator.Validate(this); - } + _provider.RunValidators(this); // TODO: Integrate with new ctor argument support. - var inject = _argumentsType.GetConstructor(new[] { typeof(CommandLineParser) }) != null; - object commandLineArguments = CreateArgumentsTypeInstance(inject); + object commandLineArguments = _provider.CreateInstance(this); foreach (CommandLineArgument argument in _arguments) { // Apply property argument values (this does nothing for constructor or method arguments). @@ -1590,24 +1601,5 @@ private CommandLineArgument GetShortArgumentOrThrow(char shortName) return null; } - - private object CreateArgumentsTypeInstance(bool inject) - { - try - { - if (inject) - { - return Activator.CreateInstance(_argumentsType, this)!; - } - else - { - return Activator.CreateInstance(_argumentsType)!; - } - } - catch (TargetInvocationException ex) - { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CreateArgumentsTypeError, ex.InnerException); - } - } } } diff --git a/src/Ookii.CommandLine/Support/IArgumentProvider.cs b/src/Ookii.CommandLine/Support/IArgumentProvider.cs new file mode 100644 index 00000000..c89f232d --- /dev/null +++ b/src/Ookii.CommandLine/Support/IArgumentProvider.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace Ookii.CommandLine.Support; + +public interface IArgumentProvider +{ + public Type ArgumentsType { get; } + + public string ApplicationFriendlyName { get; } + + public string Description { get; } + + public ParseOptionsAttribute? OptionsAttribute { get; } + + public IEnumerable GetArguments(CommandLineParser parser); + + public void RunValidators(CommandLineParser parser); + + public object CreateInstance(CommandLineParser parser); +} diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs new file mode 100644 index 00000000..900ba2b0 --- /dev/null +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -0,0 +1,78 @@ +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine.Support; + +internal class ReflectionArgumentProvider : IArgumentProvider +{ + private readonly Type _type; + + public ReflectionArgumentProvider(Type type) + { + _type = type; + } + + public Type ArgumentsType => _type; + + public ParseOptionsAttribute? OptionsAttribute => _type.GetCustomAttribute(); + + public string ApplicationFriendlyName + { + get + { + var attribute = _type.GetCustomAttribute() ?? + _type.Assembly.GetCustomAttribute(); + + return attribute?.Name ?? _type.Assembly.GetName().Name ?? string.Empty; + } + } + + public string Description => _type.GetCustomAttribute()?.Description ?? string.Empty; + + public object CreateInstance(CommandLineParser parser) + { + var inject = _type.GetConstructor(new[] { typeof(CommandLineParser) }) != null; + try + { + if (inject) + { + return Activator.CreateInstance(_type, parser)!; + } + else + { + return Activator.CreateInstance(_type)!; + } + } + catch (TargetInvocationException ex) + { + throw parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.CreateArgumentsTypeError, ex.InnerException); + } + } + + public IEnumerable GetArguments(CommandLineParser parser) + { + var properties = _type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => Attribute.IsDefined(p, typeof(CommandLineArgumentAttribute))) + .Select(p => ReflectionArgument.Create(parser, p)); + + var methods = _type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => Attribute.IsDefined(m, typeof(CommandLineArgumentAttribute))) + .Select(m => ReflectionArgument.Create(parser, m)); + + return properties.Concat(methods); + } + + public void RunValidators(CommandLineParser parser) + { + foreach (var validator in _type.GetCustomAttributes()) + { + validator.Validate(parser); + } + } +} From e5a30243fd886861493183e6211a662b00f6ba50 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Sat, 1 Apr 2023 17:11:12 -0700 Subject: [PATCH 010/234] Tested trimmed executable with custom argument provider. --- src/Ookii.CommandLine.sln | 7 +++ src/Ookii.CommandLine/CommandLineParser.cs | 2 +- .../Support/CustomArgument.cs | 43 +++++++++++++++++++ .../Support/IArgumentProvider.cs | 2 + .../Support/ReflectionArgumentProvider.cs | 5 ++- src/Ookii.CommandLine/TypeHelper.cs | 1 + src/Samples/TrimTest/Program.cs | 42 ++++++++++++++++++ src/Samples/TrimTest/TrimTest.csproj | 16 +++++++ 8 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/Ookii.CommandLine/Support/CustomArgument.cs create mode 100644 src/Samples/TrimTest/Program.cs create mode 100644 src/Samples/TrimTest/TrimTest.csproj diff --git a/src/Ookii.CommandLine.sln b/src/Ookii.CommandLine.sln index 469d689b..be838c12 100644 --- a/src/Ookii.CommandLine.sln +++ b/src/Ookii.CommandLine.sln @@ -32,6 +32,8 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrimTest", "Samples\TrimTest\TrimTest.csproj", "{D3422CDA-6FAF-4EED-9CB1-814A0D519373}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -74,6 +76,10 @@ 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 + {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -86,6 +92,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} + {D3422CDA-6FAF-4EED-9CB1-814A0D519373} = {DC9CCD22-9B9B-4298-8C68-BC7A5A680F93} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6E22AD53-E031-474F-8AC7-B247C4311820} diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index ce6c2e34..5abf578c 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1289,7 +1289,7 @@ private void DetermineAutomaticArguments() } bool autoVersion = Options.AutoVersionArgument ?? true; - if (autoVersion && !CommandInfo.IsCommand(_provider.ArgumentsType)) + if (autoVersion && !_provider.IsCommand) { var argument = CommandLineArgument.CreateAutomaticVersion(this); diff --git a/src/Ookii.CommandLine/Support/CustomArgument.cs b/src/Ookii.CommandLine/Support/CustomArgument.cs new file mode 100644 index 00000000..8d6a673a --- /dev/null +++ b/src/Ookii.CommandLine/Support/CustomArgument.cs @@ -0,0 +1,43 @@ +using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine.Support; + +// This is just a test placeholder. +public class CustomArgument : CommandLineArgument +{ + private readonly Action _setProperty; + + private CustomArgument(ArgumentInfo info, Action setProperty) : base(info) + { + _setProperty = setProperty; + } + + public static CustomArgument Create(CommandLineParser parser, string name, Type type, Action setProperty) + { + var info = new ArgumentInfo() + { + Parser = parser, + ArgumentName = name, + Kind = ArgumentKind.SingleValue, + ArgumentType = type, + ElementTypeWithNullable = type, + ElementType = type, + Converter = new StringConverter(), + Validators = Enumerable.Empty(), + }; + + return new CustomArgument(info, setProperty); + } + + protected override bool CanSetProperty => true; + + protected override bool CallMethod(object? value) => throw new NotImplementedException(); + protected override object? GetProperty(object target) => throw new NotImplementedException(); + protected override void SetProperty(object target, object? value) => _setProperty(target, value); +} diff --git a/src/Ookii.CommandLine/Support/IArgumentProvider.cs b/src/Ookii.CommandLine/Support/IArgumentProvider.cs index c89f232d..fcccc504 100644 --- a/src/Ookii.CommandLine/Support/IArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/IArgumentProvider.cs @@ -13,6 +13,8 @@ public interface IArgumentProvider public ParseOptionsAttribute? OptionsAttribute { get; } + public bool IsCommand { get; } + public IEnumerable GetArguments(CommandLineParser parser); public void RunValidators(CommandLineParser parser); diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs index 900ba2b0..917f45cc 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -1,4 +1,5 @@ -using Ookii.CommandLine.Validation; +using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; using System.ComponentModel; @@ -35,6 +36,8 @@ public string ApplicationFriendlyName public string Description => _type.GetCustomAttribute()?.Description ?? string.Empty; + public bool IsCommand => CommandInfo.IsCommand(_type); + public object CreateInstance(CommandLineParser parser) { var inject = _type.GetConstructor(new[] { typeof(CommandLineParser) }) != null; diff --git a/src/Ookii.CommandLine/TypeHelper.cs b/src/Ookii.CommandLine/TypeHelper.cs index d363c806..242fcbb1 100644 --- a/src/Ookii.CommandLine/TypeHelper.cs +++ b/src/Ookii.CommandLine/TypeHelper.cs @@ -1,6 +1,7 @@ // Copyright (c) Sven Groot (Ookii.org) using Ookii.CommandLine.Conversion; using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs new file mode 100644 index 00000000..ae149380 --- /dev/null +++ b/src/Samples/TrimTest/Program.cs @@ -0,0 +1,42 @@ +// See https://aka.ms/new-console-template for more information +using Ookii.CommandLine; +using Ookii.CommandLine.Support; + +var parser = new CommandLineParser(new MyProvider()); +var arguments = (Arguments?)parser.ParseWithErrorHandling(); +if (arguments != null) +{ + Console.WriteLine($"Hello, World! {arguments.Test}"); +} + + +class Arguments +{ + [CommandLineArgument] + public string? Test { get; set; } +} + +class MyProvider : IArgumentProvider +{ + public Type ArgumentsType => typeof(Arguments); + + public string ApplicationFriendlyName => "Test"; + + public string Description => string.Empty; + + public ParseOptionsAttribute? OptionsAttribute => null; + + public bool IsCommand => false; + + public object CreateInstance(CommandLineParser parser) + { + return new Arguments(); + } + public IEnumerable GetArguments(CommandLineParser parser) + { + yield return CustomArgument.Create(parser, "Test", typeof(string), (target, value) => ((Arguments)target).Test = (string?)value); + } + public void RunValidators(CommandLineParser parser) + { + } +} diff --git a/src/Samples/TrimTest/TrimTest.csproj b/src/Samples/TrimTest/TrimTest.csproj new file mode 100644 index 00000000..8b1b3f47 --- /dev/null +++ b/src/Samples/TrimTest/TrimTest.csproj @@ -0,0 +1,16 @@ + + + + Exe + net7.0 + enable + enable + true + true + + + + + + + From e60bef283047d74e56cbe97a1201723bceae8572 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Sun, 2 Apr 2023 17:19:08 -0700 Subject: [PATCH 011/234] Add types to be used by source generator. --- src/Ookii.CommandLine/CommandLineArgument.cs | 2708 ++++++++-------- src/Ookii.CommandLine/CommandLineParser.cs | 2788 +++++++++-------- .../Conversion/ArgumentConverter.cs | 4 +- .../Conversion/BooleanConverter.cs | 12 +- .../Conversion/ParsableConverter.cs | 30 + .../Conversion/SpanParsableConverter.cs | 29 +- .../Conversion/StringConverter.cs | 20 +- src/Ookii.CommandLine/ReflectionArgument.cs | 48 +- .../Support/ArgumentProvider.cs | 109 + .../Support/CustomArgument.cs | 43 - .../Support/GeneratedArgument.cs | 87 + .../Support/GeneratedArgumentProvider.cs | 54 + .../Support/IArgumentProvider.cs | 23 - .../Support/ReflectionArgumentProvider.cs | 44 +- src/Samples/TrimTest/Program.cs | 28 +- 15 files changed, 3137 insertions(+), 2890 deletions(-) create mode 100644 src/Ookii.CommandLine/Conversion/ParsableConverter.cs create mode 100644 src/Ookii.CommandLine/Support/ArgumentProvider.cs delete mode 100644 src/Ookii.CommandLine/Support/CustomArgument.cs create mode 100644 src/Ookii.CommandLine/Support/GeneratedArgument.cs create mode 100644 src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs delete mode 100644 src/Ookii.CommandLine/Support/IArgumentProvider.cs diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 93a17f9f..0f571ed0 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -4,1558 +4,1606 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Diagnostics; 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 a . +/// +/// +/// +public abstract class CommandLineArgument { - /// - /// Provides information about command line arguments that are recognized by a . - /// - /// - /// - public abstract class CommandLineArgument + #region Nested types + + private interface IValueHelper { - #region Nested types + object? Value { get; } + bool 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, object? value); - void ApplyValue(CommandLineArgument argument, object target); + Value = initialValue; } - private class SingleValueHelper : IValueHelper + public object? Value { get; private set; } + + public void ApplyValue(CommandLineArgument argument, object target) { - public SingleValueHelper(object? initialValue) - { - Value = initialValue; - } + argument.SetProperty(target, Value); + } + + public bool SetValue(CommandLineArgument argument, object? value) + { + Value = value; + return true; + } + } + + 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(); - public object? Value { get; private set; } + public object? Value => _values.ToArray(); - public void ApplyValue(CommandLineArgument argument, object target) + public void ApplyValue(CommandLineArgument argument, object target) + { + if (argument.CanSetProperty) { argument.SetProperty(target, Value); + return; } - public bool SetValue(CommandLineArgument argument, object? value) + var list = (ICollection?)argument.GetProperty(target) ?? throw new InvalidOperationException(); + list.Clear(); + foreach (var value in _values) { - Value = value; - return true; + list.Add(value); } } - private class MultiValueHelper : IValueHelper + public bool 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(); + _values.Add((T?)value); + return true; + } + } - public object? Value => _values.ToArray(); + 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; - public void ApplyValue(CommandLineArgument argument, object target) - { - if (argument.CanSetProperty) - { - argument.SetProperty(target, Value); - return; - } + public DictionaryValueHelper(bool allowDuplicateKeys, bool allowNullValues) + { + _allowDuplicateKeys = allowDuplicateKeys; + _allowNullValues = allowNullValues; + } - var list = (ICollection?)argument.GetProperty(target) ?? throw new InvalidOperationException(); - list.Clear(); - foreach (var value in _values) - { - list.Add(value); - } + public object? Value => _dictionary; + + public void ApplyValue(CommandLineArgument argument, object target) + { + if (argument.CanSetProperty) + { + argument.SetProperty(target, _dictionary); + return; } - public bool SetValue(CommandLineArgument argument, object? value) + var dictionary = (IDictionary?)argument.GetProperty(target) + ?? throw new InvalidOperationException(); + + dictionary.Clear(); + foreach (var pair in _dictionary) { - _values.Add((T?)value); - return true; + dictionary.Add(pair.Key, pair.Value); } } - private class DictionaryValueHelper : IValueHelper - where TKey : notnull + public bool SetValue(CommandLineArgument argument, object? value) { - // 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; + // ConvertToArgumentType is guaranteed to return non-null for dictionary arguments. + var pair = (KeyValuePair)value!; - public DictionaryValueHelper(bool allowDuplicateKeys, bool allowNullValues) + // 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)) { - _allowDuplicateKeys = allowDuplicateKeys; - _allowNullValues = allowNullValues; + throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, argument); } - public object? Value => _dictionary; - - public void ApplyValue(CommandLineArgument argument, object target) + try { - if (argument.CanSetProperty) + if (_allowDuplicateKeys) { - argument.SetProperty(target, _dictionary); - return; + _dictionary[pair.Key] = pair.Value; } - - var dictionary = (IDictionary?)argument.GetProperty(target) - ?? throw new InvalidOperationException(); - - dictionary.Clear(); - foreach (var pair in _dictionary) + else { - dictionary.Add(pair.Key, pair.Value); + _dictionary.Add(pair.Key, pair.Value); } } - - public bool SetValue(CommandLineArgument argument, object? value) + catch (ArgumentException ex) { - // ConvertToArgumentType is guaranteed to return non-null for dictionary arguments. - var pair = (KeyValuePair)value!; + throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.InvalidDictionaryValue, ex, argument, value.ToString()); + } - // 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); - } + return true; + } + } - 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()); - } + private class MethodValueHelper : IValueHelper + { + public object? Value { get; private set; } - return true; - } + public void ApplyValue(CommandLineArgument argument, object target) + { + throw new InvalidOperationException(); } - private class MethodValueHelper : IValueHelper + public bool SetValue(CommandLineArgument argument, object? value) { - public object? Value { get; private set; } - - public void ApplyValue(CommandLineArgument argument, object target) - { - throw new InvalidOperationException(); - } - - public bool SetValue(CommandLineArgument argument, object? value) - { - Value = value; - return argument.CallMethod(value); - } + Value = value; + return argument.CallMethod(value); } + } - private class HelpArgument : CommandLineArgument - { - public HelpArgument(CommandLineParser parser, string argumentName, char shortName, char shortAlias) - : base(CreateInfo(parser, argumentName, shortName, shortAlias)) - { - } + private class HelpArgument : CommandLineArgument + { + public HelpArgument(CommandLineParser parser, string argumentName, char shortName, char shortAlias) + : base(CreateInfo(parser, argumentName, shortName, shortAlias)) + { + } - protected override bool CanSetProperty => false; + protected override bool CanSetProperty => false; - private static ArgumentInfo CreateInfo(CommandLineParser parser, string argumentName, char shortName, char shortAlias) + private static ArgumentInfo CreateInfo(CommandLineParser parser, string argumentName, char shortName, char shortAlias) + { + var info = new ArgumentInfo() { - 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 = true, - Validators = Enumerable.Empty(), - Converter = BooleanConverter.Instance, - }; - - if (parser.Mode == ParsingMode.LongShort) - { - if (parser.ShortArgumentNameComparer!.Compare(shortAlias, shortName) != 0) - { - info.ShortAliases = new[] { shortAlias }; - } - } - else + 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 = true, + Validators = Enumerable.Empty(), + Converter = Conversion.BooleanConverter.Instance, + }; + + if (parser.Mode == ParsingMode.LongShort) + { + if (parser.ShortArgumentNameComparer!.Compare(shortAlias, shortName) != 0) { - var shortNameString = shortName.ToString(); - var shortAliasString = shortAlias.ToString(); - info.Aliases = string.Compare(shortAliasString, shortNameString, parser.ArgumentNameComparison) == 0 - ? new[] { shortNameString } - : new[] { shortNameString, shortAliasString }; + info.ShortAliases = new[] { shortAlias }; } - - return info; } - - protected override bool CallMethod(object? value) => true; - protected override object? GetProperty(object target) => throw new InvalidOperationException(); - protected override void SetProperty(object target, object? value) => throw new InvalidOperationException(); - } - - private class VersionArgument : CommandLineArgument - { - public VersionArgument(CommandLineParser parser, string argumentName) - : base(CreateInfo(parser, argumentName)) + else { + var shortNameString = shortName.ToString(); + var shortAliasString = shortAlias.ToString(); + info.Aliases = string.Compare(shortAliasString, shortNameString, parser.ArgumentNameComparison) == 0 + ? new[] { shortNameString } + : new[] { shortNameString, shortAliasString }; } - protected override bool CanSetProperty => false; + return info; + } - 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 = BooleanConverter.Instance - }; - } + protected override bool CallMethod(object? value) => true; + protected override object? GetProperty(object target) => throw new InvalidOperationException(); + protected override void SetProperty(object target, object? value) => throw new InvalidOperationException(); + } - protected override bool CallMethod(object? value) => AutomaticVersion(Parser); - protected override object? GetProperty(object target) => throw new InvalidOperationException(); - protected override void SetProperty(object target, object? value) => throw new InvalidOperationException(); + private class VersionArgument : CommandLineArgument + { + public VersionArgument(CommandLineParser parser, string argumentName) + : base(CreateInfo(parser, argumentName)) + { } - internal struct ArgumentInfo + protected override bool CanSetProperty => false; + + private static ArgumentInfo CreateInfo(CommandLineParser parser, string argumentName) { - 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 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 Type? KeyType { get; set; } - public Type? ValueType { get; set; } - public IEnumerable Validators { get; set; } + 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 + }; } - #endregion - - private readonly CommandLineParser _parser; - private readonly ArgumentConverter _converter; - 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 Type? _keyType; - private readonly Type? _valueType; - 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 string? _valueDescription; - private IValueHelper? _valueHelper; - private ReadOnlyMemory _usedArgumentName; - - internal CommandLineArgument(ArgumentInfo info) + protected override bool CallMethod(object? value) => AutomaticVersion(Parser); + protected override object? GetProperty(object target) => throw new InvalidOperationException(); + protected override void SetProperty(object target, object? value) => throw new InvalidOperationException(); + } + + 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 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 Type? KeyType { get; set; } + public Type? ValueType { get; set; } + public IEnumerable Validators { get; set; } + } + + #endregion + + private readonly CommandLineParser _parser; + private readonly ArgumentConverter _converter; + 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 Type? _keyType; + private readonly Type? _valueType; + 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 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) { - // 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) { - _hasLongName = info.Long; - if (info.Short) + if (info.ShortName != '\0') { - if (info.ShortName != '\0') - { - _shortName = info.ShortName; - } - else - { - _shortName = _argumentName[0]; - } + _shortName = info.ShortName; } - - if (!HasLongName) + else { - if (!HasShortName) - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoLongOrShortName, _argumentName)); - } - - _argumentName = _shortName.ToString(); + _shortName = _argumentName[0]; } } - if (HasLongName && info.Aliases != null) + if (!HasLongName) { - _aliases = new(info.Aliases.ToArray()); - } + if (!HasShortName) + { + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoLongOrShortName, _argumentName)); + } - if (HasShortName && info.ShortAliases != null) - { - _shortAliases = new(info.ShortAliases.ToArray()); + _argumentName = _shortName.ToString(); } - - _argumentType = info.ArgumentType; - _argumentKind = info.Kind; - _elementTypeWithNullable = info.ElementTypeWithNullable; - _elementType = info.ElementType; - _keyType = info.KeyType; - _valueType = info.ValueType; - _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; - _converter = info.Converter; - _defaultValue = ConvertToArgumentTypeInvariant(info.DefaultValue); - _valueDescription = info.ValueDescription; - _allowDuplicateDictionaryKeys = info.AllowDuplicateDictionaryKeys; - _allowMultiValueWhiteSpaceSeparator = IsMultiValue && !IsSwitch && info.AllowMultiValueWhiteSpaceSeparator; - _allowNull = info.AllowNull; - _keyValueSeparator = info.KeyValueSeparator; - _multiValueSeparator = info.MultiValueSeparator; } - /// - /// 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 + if (HasLongName && info.Aliases != null) { - get { return _memberName; } + _aliases = new(info.Aliases.ToArray()); } - /// - /// 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 + if (HasShortName && info.ShortAliases != null) { - get - { - var prefix = (_parser.Mode == ParsingMode.LongShort && HasLongName) - ? _parser.LongArgumentNamePrefix - : _parser.ArgumentNamePrefixes[0]; - - return prefix + _argumentName; - } + _shortAliases = new(info.ShortAliases.ToArray()); } - /// - /// 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 + _argumentType = info.ArgumentType; + _argumentKind = info.Kind; + _elementTypeWithNullable = info.ElementTypeWithNullable; + _elementType = info.ElementType; + _keyType = info.KeyType; + _valueType = info.ValueType; + _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; + _converter = info.Converter; + _defaultValue = ConvertToArgumentTypeInvariant(info.DefaultValue); + _valueDescription = info.ValueDescription; + _allowDuplicateDictionaryKeys = info.AllowDuplicateDictionaryKeys; + _allowMultiValueWhiteSpaceSeparator = IsMultiValue && !IsSwitch && info.AllowMultiValueWhiteSpaceSeparator; + _allowNull = info.AllowNull; + _keyValueSeparator = info.KeyValueSeparator; + _multiValueSeparator = info.MultiValueSeparator; + } + + /// + /// 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; } + } + + /// + /// 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 { - get - { - return (_parser.Mode == ParsingMode.LongShort && HasLongName) - ? _parser.LongArgumentNamePrefix + _argumentName - : null; - } + var prefix = (_parser.Mode == ParsingMode.LongShort && HasLongName) + ? _parser.LongArgumentNamePrefix + : _parser.ArgumentNamePrefixes[0]; + + return prefix + _argumentName; } + } - /// - /// 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 + /// + /// 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 { - get - { - return (_parser.Mode == ParsingMode.LongShort && HasShortName) - ? _parser.ArgumentNamePrefixes[0] + _shortName - : null; - } + return (_parser.Mode == ParsingMode.LongShort && HasLongName) + ? _parser.LongArgumentNamePrefix + _argumentName + : null; } + } - /// - /// 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 + /// + /// 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 { - get { return _argumentType; } + return (_parser.Mode == ParsingMode.LongShort && HasShortName) + ? _parser.ArgumentNamePrefixes[0] + _shortName + : null; } + } + + /// + /// 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; } + } + + /// + /// 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; } + } + + /// + /// 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; } + } + + /// + /// 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; } + } + + /// + /// 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 => _valueDescription ??= DetermineValueDescription(); + + /// + /// 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; } + } + + /// + /// 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 => _usedArgumentName.Length > 0 ? _usedArgumentName.ToString() : null; + + /// + /// 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; + + /// + /// 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; } + + /// + /// 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 , 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()); - /// - /// 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 + /// + /// 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 . Other types cannot be + /// converted. + /// + /// + /// 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())) { - get { return _isRequired; } + return value; } - /// - /// 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 + var stringValue = value.ToString(); + if (stringValue == null) { - get { return _defaultValue; } + return null; } - /// - /// 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 + return _converter.Convert(stringValue, CultureInfo.InvariantCulture); + } + + /// + /// 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); + + /// + /// 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 bool CallMethod(object? value); + + /// + /// Determines the value description if one wasn't explicitly given. + /// + /// + /// The type to get the description for, or null to use the value of the + /// property. + /// + /// The value description. + /// + /// + /// This method is responsible for applying the , + /// if one is specified. + /// + /// + protected virtual string DetermineValueDescription(Type? type = null) + { + if (Kind == ArgumentKind.Dictionary && type == null) { - get { return _description ?? string.Empty; } + var key = DetermineValueDescription(_keyType!.GetUnderlyingType()); + var value = DetermineValueDescription(_valueType!.GetUnderlyingType()); + return $"{key}{KeyValueSeparator}{value}"; } - /// - /// 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 => _valueDescription ??= DetermineValueDescription(); - - /// - /// 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 + var result = GetDefaultValueDescription(type); + if (result != null) { - get { return _multiValueSeparator; } + return result; } - /// - /// 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 + var typeName = GetFriendlyTypeName(type ?? ElementType); + return Parser.Options.ValueDescriptionTransform?.Apply(typeName) ?? typeName; + } + + internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, Type argumentType, bool allowsNull, + string memberName, CommandLineArgumentAttribute attribute, + MultiValueSeparatorAttribute? multiValueSeparatorAttribute, DescriptionAttribute? descriptionAttribute, + bool allowDuplicateDictionaryKeys, KeyValueSeparatorAttribute? keyValueSeparatorAttribute, + IEnumerable? aliasAttributes, IEnumerable? shortAliasAttributes, + IEnumerable? validationAttributes) + { + var argumentName = DetermineArgumentName(attribute.ArgumentName, memberName, parser.Options.ArgumentNameTransform); + return new ArgumentInfo() { - get { return _allowDuplicateDictionaryKeys; } - } + Parser = parser, + ArgumentName = argumentName, + Long = attribute.IsLong, + Short = attribute.IsShort, + ShortName = attribute.ShortName, + ArgumentType = argumentType, + ElementTypeWithNullable = argumentType, + Description = descriptionAttribute?.Description, + ValueDescription = attribute.ValueDescription, + Position = attribute.Position < 0 ? null : attribute.Position, + AllowDuplicateDictionaryKeys = allowDuplicateDictionaryKeys, + MultiValueSeparator = GetMultiValueSeparator(multiValueSeparatorAttribute), + AllowMultiValueWhiteSpaceSeparator = multiValueSeparatorAttribute != null && multiValueSeparatorAttribute.Separator == null, + KeyValueSeparator = keyValueSeparatorAttribute?.Separator, + Aliases = GetAliases(aliasAttributes, argumentName), + ShortAliases = GetShortAliases(shortAliasAttributes, argumentName), + DefaultValue = attribute.DefaultValue, + IsRequired = attribute.IsRequired, + MemberName = memberName, + AllowNull = allowsNull, + CancelParsing = attribute.CancelParsing, + IsHidden = attribute.IsHidden, + Validators = validationAttributes ?? Enumerable.Empty(), + }; + } - /// - /// 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 => _usedArgumentName.Length > 0 ? _usedArgumentName.ToString() : null; - - /// - /// 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; - - /// - /// 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; } - - /// - /// 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 , 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. - /// - /// 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 . Other types cannot be - /// converted. - /// - /// - /// 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) + 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) { - if (value == null || _elementTypeWithNullable.IsAssignableFrom(value.GetType())) + 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()) { - return value; - } + if (first) + { + first = false; + } + else + { + name.Append(", "); + } - var stringValue = value.ToString(); - if (stringValue == null) - { - return null; + name.Append(GetFriendlyTypeName(typeArgument)); } - return _converter.Convert(stringValue, CultureInfo.InvariantCulture); + name.Append('>'); + return name.ToString(); } - - /// - /// 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() + else { - return (new UsageWriter()).GetArgumentUsage(this); + return type.Name; } + } - /// - /// 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); - - /// - /// 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 bool CallMethod(object? value); - - /// - /// Determines the value description if one wasn't explicitly given. - /// - /// - /// The type to get the description for, or null to use the value of the - /// property. - /// - /// The value description. - /// - /// - /// This method is responsible for applying the , - /// if one is specified. - /// - /// - protected virtual string DetermineValueDescription(Type? type = null) + internal object? ConvertToArgumentType(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) + { + if (culture == null) { - if (Kind == ArgumentKind.Dictionary && type == null) - { - var key = DetermineValueDescription(_keyType!.GetUnderlyingType()); - var value = DetermineValueDescription(_valueType!.GetUnderlyingType()); - return $"{key}{KeyValueSeparator}{value}"; - } - - var result = GetDefaultValueDescription(type); - if (result != null) - { - return result; - } - - var typeName = GetFriendlyTypeName(type ?? ElementType); - return Parser.Options.ValueDescriptionTransform?.Apply(typeName) ?? typeName; + throw new ArgumentNullException(nameof(culture)); } - private static string GetFriendlyTypeName(Type type) + if (!hasValue) { - // This is used to generate a value description from a type name if no custom value description was supplied. - if (type.IsGenericType) + if (IsSwitch) { - 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) - { - first = false; - } - else - { - name.Append(", "); - } - - name.Append(GetFriendlyTypeName(typeArgument)); - } - - name.Append('>'); - return name.ToString(); + return true; } else { - return type.Name; + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.MissingNamedArgumentValue, this); } } - internal object? ConvertToArgumentType(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) + try { - if (culture == null) - { - throw new ArgumentNullException(nameof(culture)); - } + var converted = stringValue == null + ? _converter.Convert(spanValue, culture) + : _converter.Convert(stringValue, culture); - if (!hasValue) + if (converted == null && (!_allowNull || IsDictionary)) { - if (IsSwitch) - { - return true; - } - else - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.MissingNamedArgumentValue, this); - } + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, this); } - try - { - var converted = stringValue == null - ? _converter.Convert(spanValue, culture) - : _converter.Convert(stringValue, culture); - - if (converted == null && (!_allowNull || IsDictionary)) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, this); - } + return converted; + } + catch (NotSupportedException ex) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); + } + catch (FormatException ex) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); + } + } - return converted; - } - catch (NotSupportedException ex) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); - } - catch (FormatException ex) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); - } + internal bool HasInformation(UsageWriter writer) + { + if (!string.IsNullOrEmpty(Description)) + { + return true; } - internal bool HasInformation(UsageWriter writer) + if (writer.UseAbbreviatedSyntax && Position == null) { - if (!string.IsNullOrEmpty(Description)) - { - return true; - } + return true; + } - if (writer.UseAbbreviatedSyntax && Position == null) + if (writer.UseShortNamesForSyntax) + { + if (HasLongName) { return true; } + } + else if (HasShortName) + { + return true; + } - if (writer.UseShortNamesForSyntax) - { - if (HasLongName) - { - return true; - } - } - else if (HasShortName) - { - return true; - } + if (writer.IncludeAliasInDescription && + ((Aliases != null && Aliases.Count > 0) || (ShortAliases != null && ShortAliases.Count > 0))) + { + return true; + } - if (writer.IncludeAliasInDescription && - ((Aliases != null && Aliases.Count > 0) || (ShortAliases != null && ShortAliases.Count > 0))) - { - return true; - } + if (writer.IncludeDefaultValueInDescription && DefaultValue != null) + { + return true; + } - if (writer.IncludeDefaultValueInDescription && DefaultValue != null) - { - return true; - } + if (writer.IncludeValidatorsInDescription && + _validators.Any(v => !string.IsNullOrEmpty(v.GetUsageHelp(this)))) + { + return true; + } - if (writer.IncludeValidatorsInDescription && - _validators.Any(v => !string.IsNullOrEmpty(v.GetUsageHelp(this)))) - { - return true; - } + return false; + } - return false; - } + internal bool SetValue(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) + { + _valueHelper ??= CreateValueHelper(); - internal bool SetValue(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) + bool continueParsing; + if (IsMultiValue && hasValue && MultiValueSeparator != null) { - _valueHelper ??= CreateValueHelper(); - - bool continueParsing; - if (IsMultiValue && hasValue && MultiValueSeparator != null) + continueParsing = true; + spanValue.Split(MultiValueSeparator.AsSpan(), separateValue => { - continueParsing = true; - spanValue.Split(MultiValueSeparator.AsSpan(), separateValue => + string? separateValueString = null; + PreValidate(ref separateValueString, separateValue); + var converted = ConvertToArgumentType(culture, true, separateValueString, separateValue); + continueParsing = _valueHelper.SetValue(this, converted); + if (!continueParsing) { - string? separateValueString = null; - PreValidate(ref separateValueString, separateValue); - var converted = ConvertToArgumentType(culture, true, separateValueString, separateValue); - continueParsing = _valueHelper.SetValue(this, converted); - if (!continueParsing) - { - return false; - } + return false; + } - Validate(converted, ValidationMode.AfterConversion); - return true; - }); - } - else - { - PreValidate(ref stringValue, spanValue); - var converted = ConvertToArgumentType(culture, hasValue, stringValue, spanValue); - continueParsing = _valueHelper.SetValue(this, converted); Validate(converted, ValidationMode.AfterConversion); - } - - HasValue = true; - return continueParsing; + return true; + }); } - - internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParser parser) + else { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } - - 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()))); + PreValidate(ref stringValue, spanValue); + var converted = ConvertToArgumentType(culture, hasValue, stringValue, spanValue); + continueParsing = _valueHelper.SetValue(this, converted); + Validate(converted, ValidationMode.AfterConversion); + } - if (existingArg != null) - { - return (existingArg, false); - } + HasValue = true; + return continueParsing; + } - return (new HelpArgument(parser, argumentName, shortName, shortAlias), true); + internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParser parser) + { + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); } - internal static CommandLineArgument? CreateAutomaticVersion(CommandLineParser parser) + 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()))); + + if (existingArg != null) { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } + return (existingArg, false); + } - var argumentName = DetermineArgumentName(null, parser.StringProvider.AutomaticVersionName(), parser.Options.ArgumentNameTransform); - if (parser.GetArgument(argumentName) != null) - { - return null; - } + return (new HelpArgument(parser, argumentName, shortName, shortAlias), true); + } - return new VersionArgument(parser, argumentName); + internal static CommandLineArgument? CreateAutomaticVersion(CommandLineParser parser) + { + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); } - internal object? GetConstructorParameterValue() + var argumentName = DetermineArgumentName(null, parser.StringProvider.AutomaticVersionName(), parser.Options.ArgumentNameTransform); + if (parser.GetArgument(argumentName) != null) { - return Value; + return null; } - internal void ApplyPropertyValue(object target) - { - // Do nothing for method-based values. - // TODO: Handle new style constructor parameters. - if (Kind == ArgumentKind.Method) - { - return; - } + return new VersionArgument(parser, argumentName); + } - try - { - _valueHelper?.ApplyValue(this, target); - } - catch (TargetInvocationException ex) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex.InnerException, this); - } - } + internal object? GetConstructorParameterValue() + { + return Value; + } - internal void Reset() + internal void ApplyPropertyValue(object target) + { + // Do nothing for method-based values. + // TODO: Handle new style constructor parameters. + if (Kind == ArgumentKind.Method) { - if (!IsMultiValue && _defaultValue != null) - { - _valueHelper = new SingleValueHelper(_defaultValue); - } - else - { - _valueHelper = null; - } + return; + } - HasValue = false; - _usedArgumentName = default; + try + { + _valueHelper?.ApplyValue(this, target); + } + catch (TargetInvocationException ex) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex.InnerException, this); } + } - internal static void ShowVersion(LocalizedStringProvider stringProvider, Assembly assembly, string friendlyName) + internal void Reset() + { + if (!IsMultiValue && _defaultValue != null) { - Console.WriteLine(stringProvider.ApplicationNameAndVersion(assembly, friendlyName)); - var copyright = stringProvider.ApplicationCopyright(assembly); - if (copyright != null) - { - Console.WriteLine(copyright); - } + _valueHelper = new SingleValueHelper(_defaultValue); } + else + { + _valueHelper = null; + } + + HasValue = false; + _usedArgumentName = default; + } - internal void ValidateAfterParsing() + 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 (HasValue) - { - Validate(null, ValidationMode.AfterParsing); - } - else if (IsRequired) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.MissingRequiredArgument, ArgumentName); - } + Console.WriteLine(copyright); } + } - internal void SetUsedArgumentName(ReadOnlyMemory name) + internal void ValidateAfterParsing() + { + if (HasValue) { - _usedArgumentName = name; + Validate(null, ValidationMode.AfterParsing); } + else if (IsRequired) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.MissingRequiredArgument, ArgumentName); + } + } + + internal void SetUsedArgumentName(ReadOnlyMemory name) + { + _usedArgumentName = name; + } - private IValueHelper CreateValueHelper() + private IValueHelper CreateValueHelper() + { + Debug.Assert(_valueHelper == null); + Type type; + switch (_argumentKind) { - Debug.Assert(_valueHelper == null); - Type type; - switch (_argumentKind) - { - case ArgumentKind.Dictionary: - type = typeof(DictionaryValueHelper<,>).MakeGenericType(_elementType.GetGenericArguments()); - return (IValueHelper)Activator.CreateInstance(type, _allowDuplicateDictionaryKeys, _allowNull)!; + case ArgumentKind.Dictionary: + type = typeof(DictionaryValueHelper<,>).MakeGenericType(_elementType.GetGenericArguments()); + return (IValueHelper)Activator.CreateInstance(type, _allowDuplicateDictionaryKeys, _allowNull)!; - case ArgumentKind.MultiValue: - type = typeof(MultiValueHelper<>).MakeGenericType(_elementTypeWithNullable); - return (IValueHelper)Activator.CreateInstance(type)!; + case ArgumentKind.MultiValue: + type = typeof(MultiValueHelper<>).MakeGenericType(_elementTypeWithNullable); + return (IValueHelper)Activator.CreateInstance(type)!; - case ArgumentKind.Method: - return new MethodValueHelper(); + case ArgumentKind.Method: + return new MethodValueHelper(); - default: - Debug.Assert(_defaultValue == null); - return new SingleValueHelper(null); - } + default: + Debug.Assert(_defaultValue == null); + return new SingleValueHelper(null); } + } - internal static IEnumerable? GetAliases(IEnumerable aliasAttributes, string argumentName) + internal static IEnumerable? GetAliases(IEnumerable? aliasAttributes, string argumentName) + { + if (aliasAttributes == null || !aliasAttributes.Any()) { - if (!aliasAttributes.Any()) - { - return null; - } - - return aliasAttributes.Select(alias => - { - if (string.IsNullOrEmpty(alias.Alias)) - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.EmptyAliasFormat, argumentName)); - } - - return alias.Alias; - }); + return null; } - internal static IEnumerable? GetShortAliases(IEnumerable aliasAttributes, string argumentName) + return aliasAttributes.Select(alias => { - if (!aliasAttributes.Any()) + if (string.IsNullOrEmpty(alias.Alias)) { - return null; + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.EmptyAliasFormat, argumentName)); } - return aliasAttributes.Select(alias => - { - if (alias.Alias == '\0') - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.EmptyAliasFormat, argumentName)); - } - - return alias.Alias; - }); - } + return alias.Alias; + }); + } - private static bool AutomaticVersion(CommandLineParser parser) + internal static IEnumerable? GetShortAliases(IEnumerable? aliasAttributes, string argumentName) + { + if (aliasAttributes == null || !aliasAttributes.Any()) { - ShowVersion(parser.StringProvider, parser.ArgumentsType.Assembly, parser.ApplicationFriendlyName); - - // Cancel parsing but do not show help. - return false; + return null; } - internal static string DetermineArgumentName(string? explicitName, string memberName, NameTransform? transform) + return aliasAttributes.Select(alias => { - if (explicitName != null) + if (alias.Alias == '\0') { - return explicitName; + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.EmptyAliasFormat, argumentName)); } - return transform?.Apply(memberName) ?? memberName; - } + return alias.Alias; + }); + } - private string? GetDefaultValueDescription(Type? type) + private static bool AutomaticVersion(CommandLineParser parser) + { + ShowVersion(parser.StringProvider, parser.ArgumentsType.Assembly, parser.ApplicationFriendlyName); + + // Cancel parsing but do not show help. + return false; + } + + internal static string DetermineArgumentName(string? explicitName, string memberName, NameTransform? transform) + { + if (explicitName != null) { - if (Parser.Options.DefaultValueDescriptions == null || - !Parser.Options.DefaultValueDescriptions.TryGetValue(type ?? ElementType, out string? value)) - { - return null; - } + return explicitName; + } - return value; + return transform?.Apply(memberName) ?? memberName; + } + + private string? GetDefaultValueDescription(Type? type) + { + if (Parser.Options.DefaultValueDescriptions == null || + !Parser.Options.DefaultValueDescriptions.TryGetValue(type ?? ElementType, out string? value)) + { + return null; } - private void Validate(object? value, ValidationMode mode) + return value; + } + + private void Validate(object? value, ValidationMode mode) + { + foreach (var validator in _validators) { - foreach (var validator in _validators) + if (validator.Mode == mode) { - if (validator.Mode == mode) - { - validator.Validate(this, value); - } + validator.Validate(this, value); } } + } - private void PreValidate(ref string? stringValue, ReadOnlySpan spanValue) + 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 == ValidationMode.BeforeConversion) + if (stringValue == null) { - if (stringValue == null) + if (validator.CanValidateSpan) { - if (validator.CanValidateSpan) - { - validator.ValidateSpan(this, spanValue); - continue; - } - else - { - stringValue = spanValue.ToString(); - } + validator.ValidateSpan(this, spanValue); + continue; + } + else + { + stringValue = spanValue.ToString(); } - - validator.Validate(this, stringValue); } + + validator.Validate(this, stringValue); } } } + private static string? GetMultiValueSeparator(MultiValueSeparatorAttribute? attribute) + { + var separator = attribute?.Separator; + if (string.IsNullOrEmpty(separator)) + { + return null; + } + else + { + return separator; + } + } } diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 5abf578c..e01b3333 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -14,1592 +14,1594 @@ using System.Reflection; using System.Runtime.InteropServices; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Parses command line arguments defined by a class of the specified type. +/// +/// +/// +/// The class can parse a set of command line arguments into +/// values. Which arguments are accepted is determined from 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 parameter, or a single parameter +/// with the type , which will be passed the instance of the +/// class that was used to parse the arguments when the type +/// is instantiated. +/// +/// +/// 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. +/// +/// +/// 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. +/// 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. +/// +/// +/// The derived type also provides strongly-typed instance +/// methods, if you don't wish to use the static +/// method. +/// +/// +/// 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. +/// +/// +/// 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 List _arguments = new(); + 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 string[] _argumentNamePrefixes; + private readonly string? _longArgumentNamePrefix; + + private ReadOnlyCollection? _argumentsReadOnlyWrapper; + private ReadOnlyCollection? _argumentNamePrefixesReadOnlyWrapper; + + /// + /// 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 = ':'; + /// - /// Parses command line arguments defined by a class of the specified type. + /// Gets the default prefix used for long argument names if 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 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. /// + /// + public const string DefaultLongArgumentNamePrefix = "--"; + + /// + /// Event raised when an argument is parsed from the command line. + /// + /// /// - /// The arguments type must have a constructor that has no parameter, or a single parameter - /// with the type , which will be passed the instance of the - /// class that was used to parse the arguments when the type - /// is instantiated. + /// 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. /// /// - /// 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. + /// 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 . /// /// - /// 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. + /// 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. + /// + /// /// - /// 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. + /// 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. /// /// - /// The derived type also provides strongly-typed instance - /// methods, if you don't wish to use the static - /// method. + /// 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) + : this(new ReflectionArgumentProvider(argumentsType ?? throw new ArgumentNullException(nameof(argumentsType))), 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. + /// + /// /// - /// 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. /// /// - /// 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. + /// 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. /// /// - /// - /// - /// - /// Usage documentation - public class CommandLineParser + public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null) { - #region Nested types + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _parseOptions = options ?? new(); - private sealed class CommandLineArgumentComparer : IComparer + var optionsAttribute = _provider.OptionsAttribute; + if (optionsAttribute != null) { - private readonly StringComparison _comparison; + _parseOptions.Merge(optionsAttribute); + } - public CommandLineArgumentComparer(StringComparison comparison) + _mode = _parseOptions.Mode ?? default; + var comparison = _parseOptions.ArgumentNameComparison ?? StringComparison.OrdinalIgnoreCase; + ArgumentNameComparison = comparison; + _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)) { - _comparison = comparison; + throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefix, nameof(options)); } - public int Compare(CommandLineArgument? x, CommandLineArgument? y) + var longInfo = new PrefixInfo { Prefix = _longArgumentNamePrefix, Short = false }; + prefixInfos = prefixInfos.Append(longInfo); + _argumentsByShortName = new(new CharComparer(comparison)); + } + + _sortedPrefixes = prefixInfos.OrderByDescending(info => info.Prefix.Length).ToArray(); + _argumentsByName = new(new MemoryComparer(comparison)); + + _positionalArgumentCount = DetermineMemberArguments(); + DetermineAutomaticArguments(); + // Sort the member arguments in usage order (positional first, then required + // non-positional arguments, then the rest by name. + _arguments.Sort(new CommandLineArgumentComparer(comparison)); + + 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. + /// + /// + /// 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 => _provider.ArgumentsType; + + /// + /// 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 => _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 (""). + /// + /// + /// + /// 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 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. + /// + /// + /// One of the members 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. + /// + /// + 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; } + + 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 + { + path = nameWithoutExtension ?? Path.GetFileNameWithoutExtension(path); + } + + 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) + { + usageWriter ??= _parseOptions.UsageWriter; + usageWriter.WriteParserUsage(this); + } + + /// + /// 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) + { + usageWriter ??= _parseOptions.UsageWriter; + return usageWriter.GetUsage(this, maximumLineLength: maximumLineLength); + } + + /// + /// 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() + { + // GetCommandLineArgs include the executable, so skip it. + return Parse(Environment.GetCommandLineArgs(), 1); + } + + /// + /// + /// 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) + { + try + { + HelpRequested = false; + return ParseCore(args, index); + } + catch (CommandLineArgumentException ex) + { + HelpRequested = true; + ParseResult = ParseResult.FromException(ex); + 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 . + /// + /// + /// + /// 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) + { + handler = (sender, e) => { - if (x == null) - { - if (y == null) - { - return 0; - } - else - { - return -1; - } - } - else if (y == null) - { - return 1; - } + var warning = StringProvider.DuplicateArgumentWarning(e.Argument.ArgumentName); + WriteError(_parseOptions, warning, _parseOptions.WarningColor); + }; - // 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; - } + DuplicateArgument += handler; + } - // 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; - } + var helpMode = UsageHelpRequest.Full; + object? result = null; + try + { + result = Parse(args, index); + } + catch (CommandLineArgumentException ex) + { + WriteError(_parseOptions, ex.Message, _parseOptions.ErrorColor, true); + helpMode = _parseOptions.ShowUsageOnError; + } + finally + { + if (handler != null) + { + DuplicateArgument -= handler; + } + } + + if (HelpRequested) + { + _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 . + /// + /// + /// + /// + /// + /// + /// 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 + { + // GetCommandLineArgs include the executable, so skip it. + return Parse(Environment.GetCommandLineArgs(), 1, options); + } + + /// + /// 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 + { + return (T?)ParseInternal(typeof(T), args, index, 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); + } + + /// + /// 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) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (_argumentsByName.TryGetValue(name.AsMemory(), out var argument)) + { + return argument; + } + else + { + return null; + } + } + + /// + /// 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) + { + if (_argumentsByShortName != null && _argumentsByShortName.TryGetValue(shortName, out var argument)) + { + 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 string[] GetDefaultArgumentNamePrefixes() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? new[] { "-", "/" } + : new[] { "-" }; + } + + /// + /// 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); + } - // Sort the rest by name - return string.Compare(x.ArgumentName, y.ArgumentName, _comparison); - } - } + internal static object? ParseInternal(Type argumentsType, string[] args, int index, ParseOptions? options) + { + options ??= new(); + var parser = new CommandLineParser(argumentsType, options); + return parser.ParseWithErrorHandling(args, index); + } - private sealed class MemoryComparer : IComparer> - { - private readonly StringComparison _comparison; + internal static bool ShouldIndent(LineWrappingTextWriter writer) + { + return writer.MaximumLineLength is 0 or >= 30; + } - public MemoryComparer(StringComparison comparison) + 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) { - _comparison = comparison; + error.Inner.Write(color); } - 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) + error.Inner.Write(message); + if (options.UseErrorColor ?? false) { - _comparison = comparison; + error.Inner.Write(options.UsageWriter.ColorReset); } - public int Compare(char x, char y) + error.Inner.WriteLine(); + if (blankLine) { - 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); - } + error.Inner.WriteLine(); } } - - private struct PrefixInfo + finally { - public string Prefix { get; set; } - public bool Short { get; set; } + // Reset UseErrorColor if it was changed. + if (errorVtSupport != null) + { + options.UseErrorColor = null; + } } + } - #endregion - - private readonly IArgumentProvider _provider; - private readonly List _arguments = new(); - 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 string[] _argumentNamePrefixes; - private readonly string? _longArgumentNamePrefix; - - private ReadOnlyCollection? _argumentsReadOnlyWrapper; - private ReadOnlyCollection? _argumentNamePrefixesReadOnlyWrapper; - - /// - /// 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) - : this(new ReflectionArgumentProvider(argumentsType ?? throw new ArgumentNullException(nameof(argumentsType))), options) + private static string[] DetermineArgumentNamePrefixes(ParseOptions options) + { + if (options.ArgumentNamePrefixes == null) { + return GetDefaultArgumentNamePrefixes(); } - - /// - /// 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(IArgumentProvider provider, ParseOptions? options = null) + else { - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - _parseOptions = options ?? new(); - - var optionsAttribute = _provider.OptionsAttribute; - if (optionsAttribute != null) + var result = options.ArgumentNamePrefixes.ToArray(); + if (result.Length == 0) { - _parseOptions.Merge(optionsAttribute); + throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefixes, nameof(options)); } - _mode = _parseOptions.Mode ?? default; - var comparison = _parseOptions.ArgumentNameComparison ?? StringComparison.OrdinalIgnoreCase; - ArgumentNameComparison = comparison; - _argumentNamePrefixes = DetermineArgumentNamePrefixes(_parseOptions); - var prefixInfos = _argumentNamePrefixes.Select(p => new PrefixInfo { Prefix = p, Short = true }); - if (_mode == ParsingMode.LongShort) + if (result.Any(prefix => string.IsNullOrWhiteSpace(prefix))) { - _longArgumentNamePrefix = _parseOptions.LongArgumentNamePrefix ?? DefaultLongArgumentNamePrefix; - if (string.IsNullOrWhiteSpace(_longArgumentNamePrefix)) - { - throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefix, nameof(options)); - } - - var longInfo = new PrefixInfo { Prefix = _longArgumentNamePrefix, Short = false }; - prefixInfos = prefixInfos.Append(longInfo); - _argumentsByShortName = new(new CharComparer(comparison)); + throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefix, nameof(options)); } - _sortedPrefixes = prefixInfos.OrderByDescending(info => info.Prefix.Length).ToArray(); - _argumentsByName = new(new MemoryComparer(comparison)); - - _positionalArgumentCount = DetermineMemberArguments(); - DetermineAutomaticArguments(); - // Sort the member arguments in usage order (positional first, then required - // non-positional arguments, then the rest by name. - _arguments.Sort(new CommandLineArgumentComparer(comparison)); - - VerifyPositionalArgumentRules(); + return result; } + } - /// - /// 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 => _provider.ArgumentsType; - - /// - /// 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 => _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 (""). - /// - /// - /// - /// 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 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. - /// - /// - /// One of the members 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. - /// - /// - 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; } - - 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) + private int DetermineMemberArguments() + { + int additionalPositionalArgumentCount = 0; + foreach (var argument in _provider.GetArguments(this)) { - 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 + AddNamedArgument(argument); + if (argument.Position != null) { - path = nameWithoutExtension ?? Path.GetFileNameWithoutExtension(path); + ++additionalPositionalArgumentCount; } - - 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) - { - usageWriter ??= _parseOptions.UsageWriter; - usageWriter.WriteParserUsage(this); - } + return additionalPositionalArgumentCount; + } - /// - /// 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) + private void DetermineAutomaticArguments() + { + bool autoHelp = Options.AutoHelpArgument ?? true; + if (autoHelp) { - usageWriter ??= _parseOptions.UsageWriter; - return usageWriter.GetUsage(this, maximumLineLength: maximumLineLength); - } + var (argument, created) = CommandLineArgument.CreateAutomaticHelp(this); - /// - /// 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() - { - // GetCommandLineArgs include the executable, so skip it. - return Parse(Environment.GetCommandLineArgs(), 1); + if (created) + { + AddNamedArgument(argument); + } + + HelpArgument = argument; } - /// - /// - /// 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) + bool autoVersion = Options.AutoVersionArgument ?? true; + if (autoVersion && !_provider.IsCommand) { - try - { - HelpRequested = false; - return ParseCore(args, index); - } - catch (CommandLineArgumentException ex) + var argument = CommandLineArgument.CreateAutomaticVersion(this); + + if (argument != null) { - HelpRequested = true; - ParseResult = ParseResult.FromException(ex); - throw; + AddNamedArgument(argument); } } + } - /// - /// 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() + private void AddNamedArgument(CommandLineArgument argument) + { + if (argument.ArgumentName.Contains(NameValueSeparator)) { - // GetCommandLineArgs include the executable, so skip it. - return ParseWithErrorHandling(Environment.GetCommandLineArgs(), 1); + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.ArgumentNameContainsSeparatorFormat, argument.ArgumentName)); } - /// - /// - /// 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) + if (argument.HasLongName) { - EventHandler? handler = null; - if (_parseOptions.DuplicateArguments == ErrorMode.Warning) + _argumentsByName.Add(argument.ArgumentName.AsMemory(), argument); + if (argument.Aliases != null) { - handler = (sender, e) => + foreach (string alias in argument.Aliases) { - var warning = StringProvider.DuplicateArgumentWarning(e.Argument.ArgumentName); - WriteError(_parseOptions, warning, _parseOptions.WarningColor); - }; - - DuplicateArgument += handler; + _argumentsByName.Add(alias.AsMemory(), argument); + } } + } - var helpMode = UsageHelpRequest.Full; - object? result = null; - try - { - result = Parse(args, index); - } - catch (CommandLineArgumentException ex) - { - WriteError(_parseOptions, ex.Message, _parseOptions.ErrorColor, true); - helpMode = _parseOptions.ShowUsageOnError; - } - finally + if (_argumentsByShortName != null && argument.HasShortName) + { + _argumentsByShortName.Add(argument.ShortName, argument); + if (argument.ShortAliases != null) { - if (handler != null) + foreach (var alias in argument.ShortAliases) { - DuplicateArgument -= handler; + _argumentsByShortName.Add(alias, argument); } } - - if (HelpRequested) - { - _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 . - /// - /// - /// - /// - /// - /// - /// 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 - { - // GetCommandLineArgs include the executable, so skip it. - return Parse(Environment.GetCommandLineArgs(), 1, options); - } + _arguments.Add(argument); + } - /// - /// 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 - { - return (T?)ParseInternal(typeof(T), args, index, options); - } + private void VerifyPositionalArgumentRules() + { + bool hasOptionalArgument = false; + bool hasArrayArgument = false; - /// - /// 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 + for (int x = 0; x < _positionalArgumentCount; ++x) { - return Parse(args, 0, options); - } + CommandLineArgument argument = _arguments[x]; - /// - /// 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) - { - if (name == null) + if (hasArrayArgument) { - throw new ArgumentNullException(nameof(name)); + throw new NotSupportedException(Properties.Resources.ArrayNotLastArgument); } - if (_argumentsByName.TryGetValue(name.AsMemory(), out var argument)) + if (argument.IsRequired && hasOptionalArgument) { - return argument; + throw new NotSupportedException(Properties.Resources.InvalidOptionalArgumentOrder); } - else - { - return null; - } - } - /// - /// 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) - { - if (_argumentsByShortName != null && _argumentsByShortName.TryGetValue(shortName, out var argument)) + if (!argument.IsRequired) { - return argument; + hasOptionalArgument = true; } - else + + if (argument.IsMultiValue) { - return null; + hasArrayArgument = true; } - } - /// - /// 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() - { - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? new[] { "-", "/" } - : new[] { "-" }; + argument.Position = x; } + } - /// - /// Raises the event. - /// - /// The data for the event. - protected virtual void OnArgumentParsed(ArgumentParsedEventArgs e) + private object? ParseCore(string[] args, int index) + { + if (args == null) { - ArgumentParsed?.Invoke(this, e); + throw new ArgumentNullException(nameof(index)); } - /// - /// Raises the event. - /// - /// The data for the event. - protected virtual void OnDuplicateArgument(DuplicateArgumentEventArgs e) + if (index < 0 || index > args.Length) { - DuplicateArgument?.Invoke(this, e); + throw new ArgumentOutOfRangeException(nameof(index)); } - internal static object? ParseInternal(Type argumentsType, string[] args, int index, ParseOptions? options) + // Reset all arguments to their default value. + foreach (CommandLineArgument argument in _arguments) { - options ??= new(); - var parser = new CommandLineParser(argumentsType, options); - return parser.ParseWithErrorHandling(args, index); + argument.Reset(); } - internal static bool ShouldIndent(LineWrappingTextWriter writer) - { - return writer.MaximumLineLength is 0 or >= 30; - } + HelpRequested = false; + int positionalArgumentIndex = 0; - private static void WriteError(ParseOptions options, string message, string color, bool blankLine = false) + for (int x = index; x < args.Length; ++x) { - using var errorVtSupport = options.EnableErrorColor(); - try - { - using var error = DisposableWrapper.Create(options.Error, LineWrappingTextWriter.ForConsoleError); - if (options.UseErrorColor ?? false) - { - error.Inner.Write(color); - } - - error.Inner.Write(message); - if (options.UseErrorColor ?? false) - { - error.Inner.Write(options.UsageWriter.ColorReset); - } - - error.Inner.WriteLine(); - if (blankLine) - { - error.Inner.WriteLine(); - } - } - finally + string arg = args[x]; + var argumentNamePrefix = CheckArgumentNamePrefix(arg); + if (argumentNamePrefix != null) { - // Reset UseErrorColor if it was changed. - if (errorVtSupport != 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) { - options.UseErrorColor = null; + return null; } } - } - - private static string[] DetermineArgumentNamePrefixes(ParseOptions options) - { - if (options.ArgumentNamePrefixes == null) - { - return GetDefaultArgumentNamePrefixes(); - } else { - var result = options.ArgumentNamePrefixes.ToArray(); - if (result.Length == 0) + // If this is a multi-value argument is must be the last argument. + if (positionalArgumentIndex < _positionalArgumentCount && !_arguments[positionalArgumentIndex].IsMultiValue) { - throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefixes, nameof(options)); + // Skip named positional arguments that have already been specified by name. + while (positionalArgumentIndex < _positionalArgumentCount && !_arguments[positionalArgumentIndex].IsMultiValue && _arguments[positionalArgumentIndex].HasValue) + { + ++positionalArgumentIndex; + } } - if (result.Any(prefix => string.IsNullOrWhiteSpace(prefix))) + if (positionalArgumentIndex >= _positionalArgumentCount) { - throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefix, nameof(options)); + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.TooManyArguments); } - return result; - } - } - - private int DetermineMemberArguments() - { - int additionalPositionalArgumentCount = 0; - foreach (var argument in _provider.GetArguments(this)) - { - AddNamedArgument(argument); - if (argument.Position != null) + // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler + // or the CancelParsing property. + if (ParseArgumentValue(_arguments[positionalArgumentIndex], arg, arg.AsMemory())) { - ++additionalPositionalArgumentCount; + return null; } } - - return additionalPositionalArgumentCount; } - private void DetermineAutomaticArguments() + // Check required arguments and post-parsing validation. This is done in usage order. + foreach (CommandLineArgument argument in _arguments) { - bool autoHelp = Options.AutoHelpArgument ?? true; - if (autoHelp) - { - var (argument, created) = CommandLineArgument.CreateAutomaticHelp(this); - - if (created) - { - AddNamedArgument(argument); - } - - HelpArgument = argument; - } + argument.ValidateAfterParsing(); + } - bool autoVersion = Options.AutoVersionArgument ?? true; - if (autoVersion && !_provider.IsCommand) - { - var argument = CommandLineArgument.CreateAutomaticVersion(this); + // Run class validators. + _provider.RunValidators(this); - if (argument != null) - { - AddNamedArgument(argument); - } - } + // TODO: Integrate with new ctor argument support. + object commandLineArguments = _provider.CreateInstance(this); + foreach (CommandLineArgument argument in _arguments) + { + // Apply property argument values (this does nothing for constructor or method arguments). + argument.ApplyPropertyValue(commandLineArguments); } - private void AddNamedArgument(CommandLineArgument argument) + ParseResult = ParseResult.Success; + return commandLineArguments; + } + + private bool ParseArgumentValue(CommandLineArgument argument, string? stringValue, ReadOnlyMemory? memoryValue) + { + if (argument.HasValue && !argument.IsMultiValue) { - if (argument.ArgumentName.Contains(NameValueSeparator)) + if (!AllowDuplicateArguments) { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.ArgumentNameContainsSeparatorFormat, argument.ArgumentName)); + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.DuplicateArgument, argument); } - if (argument.HasLongName) - { - _argumentsByName.Add(argument.ArgumentName.AsMemory(), argument); - if (argument.Aliases != null) - { - foreach (string alias in argument.Aliases) - { - _argumentsByName.Add(alias.AsMemory(), argument); - } - } - } + var duplicateEventArgs = stringValue == null + ? new DuplicateArgumentEventArgs(argument, memoryValue.HasValue, memoryValue ?? default) + : new DuplicateArgumentEventArgs(argument, stringValue); - if (_argumentsByShortName != null && argument.HasShortName) + OnDuplicateArgument(duplicateEventArgs); + if (duplicateEventArgs.KeepOldValue) { - _argumentsByShortName.Add(argument.ShortName, argument); - if (argument.ShortAliases != null) - { - foreach (var alias in argument.ShortAliases) - { - _argumentsByShortName.Add(alias, argument); - } - } + return false; } - - _arguments.Add(argument); } - private void VerifyPositionalArgumentRules() + bool continueParsing = argument.SetValue(Culture, memoryValue.HasValue, stringValue, (memoryValue ?? default).Span); + var e = new ArgumentParsedEventArgs(argument) { - bool hasOptionalArgument = false; - bool hasArrayArgument = false; + Cancel = !continueParsing + }; - 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); - } - - if (!argument.IsRequired) - { - hasOptionalArgument = true; - } - - if (argument.IsMultiValue) - { - hasArrayArgument = true; - } + OnArgumentParsed(e); + var cancel = e.Cancel || (argument.CancelParsing && !e.OverrideCancelParsing); - argument.Position = x; - } + // Automatically request help only if the cancellation was not due to the SetValue + // call. + if (continueParsing) + { + HelpRequested = cancel; } - private object? ParseCore(string[] args, int index) + if (cancel) { - if (args == null) - { - throw new ArgumentNullException(nameof(index)); - } - - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - // Reset all arguments to their default value. - foreach (CommandLineArgument argument in _arguments) - { - argument.Reset(); - } - - 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; - } - } + ParseResult = ParseResult.FromCanceled(argument.ArgumentName); + } - if (positionalArgumentIndex >= _positionalArgumentCount) - { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.TooManyArguments); - } + return cancel; + } - // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler - // or the CancelParsing property. - if (ParseArgumentValue(_arguments[positionalArgumentIndex], arg, arg.AsMemory())) - { - return null; - } - } - } + private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) + { + var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitOnce(NameValueSeparator); - // Check required arguments and post-parsing validation. This is done in usage order. - foreach (CommandLineArgument argument in _arguments) + CommandLineArgument? argument = null; + if (_argumentsByShortName != null && prefix.Short) + { + if (argumentName.Length == 1) { - argument.ValidateAfterParsing(); + argument = GetShortArgumentOrThrow(argumentName.Span[0]); } - - // Run class validators. - _provider.RunValidators(this); - - // TODO: Integrate with new ctor argument support. - object commandLineArguments = _provider.CreateInstance(this); - foreach (CommandLineArgument argument in _arguments) + else { - // Apply property argument values (this does nothing for constructor or method arguments). - argument.ApplyPropertyValue(commandLineArguments); + // ParseShortArgument returns true if parsing was canceled by the + // ArgumentParsed event handler or the CancelParsing property. + return ParseShortArgument(argumentName.Span, argumentValue) ? -1 : index; } + } - ParseResult = ParseResult.Success; - return commandLineArguments; + if (argument == null && !_argumentsByName.TryGetValue(argumentName, out argument)) + { + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, argumentName.ToString()); } - private bool ParseArgumentValue(CommandLineArgument argument, string? stringValue, ReadOnlyMemory? memoryValue) + 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]; + + // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed + // event handler or the CancelParsing property. + if (ParseArgumentValue(argument, argumentValueString, argumentValueString.AsMemory())) { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.DuplicateArgument, argument); + return -1; } - var duplicateEventArgs = stringValue == null - ? new DuplicateArgumentEventArgs(argument, memoryValue.HasValue, memoryValue ?? default) - : new DuplicateArgumentEventArgs(argument, stringValue); - - OnDuplicateArgument(duplicateEventArgs); - if (duplicateEventArgs.KeepOldValue) + if (!argument.AllowMultiValueWhiteSpaceSeparator) { - return false; + break; } } - bool continueParsing = argument.SetValue(Culture, memoryValue.HasValue, stringValue, (memoryValue ?? default).Span); - 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 index; } - - return cancel; } - private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) - { - var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitOnce(NameValueSeparator); - - CommandLineArgument? argument = null; - if (_argumentsByShortName != null && prefix.Short) - { - if (argumentName.Length == 1) - { - argument = GetShortArgumentOrThrow(argumentName.Span[0]); - } - else - { - // ParseShortArgument returns true if parsing was canceled by the - // ArgumentParsed event handler or the CancelParsing property. - return ParseShortArgument(argumentName.Span, argumentValue) ? -1 : index; - } - } + // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler + // or the CancelParsing property. + return ParseArgumentValue(argument, null, argumentValue) ? -1 : index; + } - if (argument == null && !_argumentsByName.TryGetValue(argumentName, out argument)) + private bool ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? value) + { + foreach (var ch in name) + { + var arg = GetShortArgumentOrThrow(ch); + if (!arg.IsSwitch) { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, argumentName.ToString()); + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, name.ToString()); } - argument.SetUsedArgumentName(argumentName); - if (!argumentValue.HasValue && !argument.IsSwitch && AllowWhiteSpaceValueSeparator) + if (ParseArgumentValue(arg, null, value)) { - 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) - { - ++index; - argumentValueString = args[index]; - - // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed - // event handler or the CancelParsing property. - if (ParseArgumentValue(argument, argumentValueString, argumentValueString.AsMemory())) - { - return -1; - } - - if (!argument.AllowMultiValueWhiteSpaceSeparator) - { - break; - } - } - - if (argumentValueString != null) - { - return index; - } + return true; } - - // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler - // or the CancelParsing property. - return ParseArgumentValue(argument, null, argumentValue) ? -1 : index; } - private bool ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? value) - { - foreach (var ch in name) - { - var arg = GetShortArgumentOrThrow(ch); - if (!arg.IsSwitch) - { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, name.ToString()); - } - - if (ParseArgumentValue(arg, null, value)) - { - return true; - } - } + return false; + } - return false; + private CommandLineArgument GetShortArgumentOrThrow(char shortName) + { + if (_argumentsByShortName!.TryGetValue(shortName, out CommandLineArgument? argument)) + { + return argument; } - private CommandLineArgument GetShortArgumentOrThrow(char shortName) - { - if (_argumentsByShortName!.TryGetValue(shortName, out CommandLineArgument? argument)) - { - return argument; - } + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, shortName.ToString()); + } - 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 PrefixInfo? CheckArgumentNamePrefix(string argument) + foreach (var prefix in _sortedPrefixes) { - // 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)) + if (argument.StartsWith(prefix.Prefix, StringComparison.Ordinal)) { - return null; + return prefix; } - - foreach (var prefix in _sortedPrefixes) - { - if (argument.StartsWith(prefix.Prefix, StringComparison.Ordinal)) - { - return prefix; - } - } - - return null; } + + return null; } } diff --git a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs index c3077dd2..021d7e24 100644 --- a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs @@ -17,7 +17,7 @@ namespace Ookii.CommandLine.Conversion; public abstract class ArgumentConverter { /// - /// Convert a string to the type of the argument. + /// Converts a string to the type of the argument. /// /// The string to convert. /// The culture to use for the conversion. @@ -28,7 +28,7 @@ public abstract class ArgumentConverter public abstract object? Convert(string value, CultureInfo culture); /// - /// Convert a string to the type of the argument. + /// Converts a string to the type of the argument. /// /// The containing the string to convert. /// The culture to use for the conversion. diff --git a/src/Ookii.CommandLine/Conversion/BooleanConverter.cs b/src/Ookii.CommandLine/Conversion/BooleanConverter.cs index 7313cf5e..1b1a2124 100644 --- a/src/Ookii.CommandLine/Conversion/BooleanConverter.cs +++ b/src/Ookii.CommandLine/Conversion/BooleanConverter.cs @@ -3,14 +3,22 @@ namespace Ookii.CommandLine.Conversion; -// Boolean doesn't support ISpanParsable, so special-case it. -internal class BooleanConverter : ArgumentConverter +/// +/// Converter for arguments with boolean values. These are typically switch arguments. +/// +/// +public class BooleanConverter : ArgumentConverter { + /// + /// A default instance of the converter. + /// public static readonly BooleanConverter Instance = new(); + /// public override object? Convert(string value, CultureInfo culture) => bool.Parse(value); #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + /// public override object? Convert(ReadOnlySpan value, CultureInfo culture) => bool.Parse(value); #endif } diff --git a/src/Ookii.CommandLine/Conversion/ParsableConverter.cs b/src/Ookii.CommandLine/Conversion/ParsableConverter.cs new file mode 100644 index 00000000..e755bae4 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/ParsableConverter.cs @@ -0,0 +1,30 @@ +#if NET7_0_OR_GREATER + +using System; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// An argument converter for types that implement . +/// +/// The type to convert. +/// +/// +/// Conversion is performed using the method. +/// +/// +/// Only use this converter for types that implement , but not +/// . For types that implement , +/// use the . +/// +/// +/// +public class ParsableConverter : ArgumentConverter + where T : IParsable +{ + /// + public override object? Convert(string value, CultureInfo culture) => T.Parse(value, culture); +} + +#endif diff --git a/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs index 61fc90e4..1bc6b0b3 100644 --- a/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs @@ -3,15 +3,30 @@ using System; using System.Globalization; -namespace Ookii.CommandLine.Conversion +namespace Ookii.CommandLine.Conversion; + +/// +/// An argument converter for types that implement . +/// +/// The type to convert. +/// +/// +/// Conversion is performed using the method. +/// +/// +/// For types that implement , but not , +/// use the . +/// +/// +/// +public class SpanParsableConverter : ArgumentConverter + where T : ISpanParsable { - internal class SpanParsableConverter : ArgumentConverter - where T : ISpanParsable - { - public override object? Convert(string value, CultureInfo culture) => T.Parse(value, culture); + /// + public override object? Convert(string value, CultureInfo culture) => T.Parse(value, culture); - public override object? Convert(ReadOnlySpan value, CultureInfo culture) => T.Parse(value, culture); - } + /// + public override object? Convert(ReadOnlySpan value, CultureInfo culture) => T.Parse(value, culture); } #endif diff --git a/src/Ookii.CommandLine/Conversion/StringConverter.cs b/src/Ookii.CommandLine/Conversion/StringConverter.cs index d2aa6b64..1bd1de8c 100644 --- a/src/Ookii.CommandLine/Conversion/StringConverter.cs +++ b/src/Ookii.CommandLine/Conversion/StringConverter.cs @@ -1,16 +1,24 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Ookii.CommandLine.Conversion; -// Identity converter for strings. -internal class StringConverter : ArgumentConverter +/// +/// A converter for arguments with string values. +/// +/// +/// This converter does not performan 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(); + /// public override object? Convert(string value, CultureInfo culture) => value; } diff --git a/src/Ookii.CommandLine/ReflectionArgument.cs b/src/Ookii.CommandLine/ReflectionArgument.cs index 8bbfe954..13407902 100644 --- a/src/Ookii.CommandLine/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/ReflectionArgument.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Text; using Ookii.CommandLine.Validation; +using System.Threading; namespace Ookii.CommandLine; @@ -127,35 +128,15 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo var attribute = member.GetCustomAttribute() ?? throw new ArgumentException(Properties.Resources.MissingArgumentAttribute, nameof(method)); - var argumentName = DetermineArgumentName(attribute.ArgumentName, member.Name, parser.Options.ArgumentNameTransform); var multiValueSeparatorAttribute = member.GetCustomAttribute(); + var descriptionAttribute = 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(); - var info = new ArgumentInfo() - { - Parser = parser, - ArgumentName = argumentName, - Long = attribute.IsLong, - Short = attribute.IsShort, - ShortName = attribute.ShortName, - ArgumentType = argumentType, - ElementTypeWithNullable = argumentType, - Description = member.GetCustomAttribute()?.Description, - ValueDescription = attribute.ValueDescription, - Position = attribute.Position < 0 ? null : attribute.Position, - AllowDuplicateDictionaryKeys = Attribute.IsDefined(member, typeof(AllowDuplicateDictionaryKeysAttribute)), - 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(), - }; + ArgumentInfo info = CreateArgumentInfo(parser, argumentType, allowsNull, member.Name, attribute, multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); DetermineAdditionalInfo(ref info, member); return new ReflectionArgument(info, property, method); @@ -417,17 +398,4 @@ private static (MethodArgumentInfo, Type, bool)? DetermineMethodArgumentInfo(Met return (info, argumentType, allowsNull); } - - private static string? GetMultiValueSeparator(MultiValueSeparatorAttribute? attribute) - { - var separator = attribute?.Separator; - if (string.IsNullOrEmpty(separator)) - { - return null; - } - else - { - return separator; - } - } } diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs new file mode 100644 index 00000000..27578815 --- /dev/null +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -0,0 +1,109 @@ +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Ookii.CommandLine.Support; + +/// +/// A source of arguments for the . +/// +/// +/// This interface is used by the source generator when using +/// attribute. It should not normally be used by regular 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 ?? throw new ArgumentNullException(nameof(validators)); + } + + /// + /// 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 shell command. + /// + /// + /// if the arguments type is a shell command; 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 instance of the type indicated by . + public abstract object CreateInstance(CommandLineParser parser); +} diff --git a/src/Ookii.CommandLine/Support/CustomArgument.cs b/src/Ookii.CommandLine/Support/CustomArgument.cs deleted file mode 100644 index 8d6a673a..00000000 --- a/src/Ookii.CommandLine/Support/CustomArgument.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Ookii.CommandLine.Conversion; -using Ookii.CommandLine.Validation; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Ookii.CommandLine.Support; - -// This is just a test placeholder. -public class CustomArgument : CommandLineArgument -{ - private readonly Action _setProperty; - - private CustomArgument(ArgumentInfo info, Action setProperty) : base(info) - { - _setProperty = setProperty; - } - - public static CustomArgument Create(CommandLineParser parser, string name, Type type, Action setProperty) - { - var info = new ArgumentInfo() - { - Parser = parser, - ArgumentName = name, - Kind = ArgumentKind.SingleValue, - ArgumentType = type, - ElementTypeWithNullable = type, - ElementType = type, - Converter = new StringConverter(), - Validators = Enumerable.Empty(), - }; - - return new CustomArgument(info, setProperty); - } - - protected override bool CanSetProperty => true; - - protected override bool CallMethod(object? value) => throw new NotImplementedException(); - protected override object? GetProperty(object target) => throw new NotImplementedException(); - protected override void SetProperty(object target, object? value) => _setProperty(target, value); -} diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs new file mode 100644 index 00000000..9d8efa01 --- /dev/null +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -0,0 +1,87 @@ +using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine.Support; + +/// +/// This class is for internal use by the source generator, and should not be used in your code. +/// +public class GeneratedArgument : CommandLineArgument +{ + private readonly Action? _setProperty; + + private GeneratedArgument(ArgumentInfo info, Action? setProperty) : base(info) + { + _setProperty = setProperty; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static GeneratedArgument Create(CommandLineParser parser, + Type argumentType, + string memberName, + CommandLineArgumentAttribute attribute, + ArgumentConverter converter, + bool allowsNull = false, + MultiValueSeparatorAttribute? multiValueSeparatorAttribute = null, + DescriptionAttribute? descriptionAttribute = null, + bool allowDuplicateDictionaryKeys = false, + KeyValueSeparatorAttribute? keyValueSeparatorAttribute = null, + IEnumerable? aliasAttributes = null, + IEnumerable? shortAliasAttributes = null, + IEnumerable? validationAttributes = null, + Action? setProperty = null) + { + var info = CreateArgumentInfo(parser, argumentType, allowsNull, memberName, attribute, + multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, + aliasAttributes, shortAliasAttributes, validationAttributes); + + // TODO: Set property for multi-value and Nullable. + info.ElementType = argumentType; + info.Converter = converter; + + return new GeneratedArgument(info, setProperty); + } + + /// + protected override bool CanSetProperty => true; + + /// + protected override bool CallMethod(object? value) => throw new NotImplementedException(); + + /// + protected override object? GetProperty(object target) => throw new NotImplementedException(); + + /// + protected override void SetProperty(object target, object? value) + { + if (_setProperty == null) + { + throw new InvalidOperationException(); + } + + _setProperty(target, value); + } +} diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs new file mode 100644 index 00000000..2880175d --- /dev/null +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -0,0 +1,54 @@ +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine.Support; + +/// +/// A base class for argument providers created by the . +/// +/// +/// This type is for internal use and should not be used by your 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, + IEnumerable validators, ApplicationFriendlyNameAttribute? friendlyName, + DescriptionAttribute? description) + : base(argumentsType, options, validators) + { + _friendlyNameAttribute = friendlyName; + _descriptionAttribute = description; + } + + /// + public override string ApplicationFriendlyName + => _friendlyNameAttribute?.Name ?? ArgumentsType.Assembly.GetName().Name ?? string.Empty; + + /// + public override string Description => _descriptionAttribute?.Description ?? string.Empty; +} diff --git a/src/Ookii.CommandLine/Support/IArgumentProvider.cs b/src/Ookii.CommandLine/Support/IArgumentProvider.cs deleted file mode 100644 index fcccc504..00000000 --- a/src/Ookii.CommandLine/Support/IArgumentProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Ookii.CommandLine.Support; - -public interface IArgumentProvider -{ - public Type ArgumentsType { get; } - - public string ApplicationFriendlyName { get; } - - public string Description { get; } - - public ParseOptionsAttribute? OptionsAttribute { get; } - - public bool IsCommand { get; } - - public IEnumerable GetArguments(CommandLineParser parser); - - public void RunValidators(CommandLineParser parser); - - public object CreateInstance(CommandLineParser parser); -} diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs index 917f45cc..a2521f8c 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -10,46 +10,40 @@ namespace Ookii.CommandLine.Support; -internal class ReflectionArgumentProvider : IArgumentProvider +internal class ReflectionArgumentProvider : ArgumentProvider { - private readonly Type _type; - public ReflectionArgumentProvider(Type type) + : base(type, type.GetCustomAttribute(), type.GetCustomAttributes()) { - _type = type; } - public Type ArgumentsType => _type; - - public ParseOptionsAttribute? OptionsAttribute => _type.GetCustomAttribute(); - - public string ApplicationFriendlyName + public override string ApplicationFriendlyName { get { - var attribute = _type.GetCustomAttribute() ?? - _type.Assembly.GetCustomAttribute(); + var attribute = ArgumentsType.GetCustomAttribute() ?? + ArgumentsType.Assembly.GetCustomAttribute(); - return attribute?.Name ?? _type.Assembly.GetName().Name ?? string.Empty; + return attribute?.Name ?? ArgumentsType.Assembly.GetName().Name ?? string.Empty; } } - public string Description => _type.GetCustomAttribute()?.Description ?? string.Empty; + public override string Description => ArgumentsType.GetCustomAttribute()?.Description ?? string.Empty; - public bool IsCommand => CommandInfo.IsCommand(_type); + public override bool IsCommand => CommandInfo.IsCommand(ArgumentsType); - public object CreateInstance(CommandLineParser parser) + public override object CreateInstance(CommandLineParser parser) { - var inject = _type.GetConstructor(new[] { typeof(CommandLineParser) }) != null; + var inject = ArgumentsType.GetConstructor(new[] { typeof(CommandLineParser) }) != null; try { if (inject) { - return Activator.CreateInstance(_type, parser)!; + return Activator.CreateInstance(ArgumentsType, parser)!; } else { - return Activator.CreateInstance(_type)!; + return Activator.CreateInstance(ArgumentsType)!; } } catch (TargetInvocationException ex) @@ -58,24 +52,16 @@ public object CreateInstance(CommandLineParser parser) } } - public IEnumerable GetArguments(CommandLineParser parser) + public override IEnumerable GetArguments(CommandLineParser parser) { - var properties = _type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + var properties = ArgumentsType.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => Attribute.IsDefined(p, typeof(CommandLineArgumentAttribute))) .Select(p => ReflectionArgument.Create(parser, p)); - var methods = _type.GetMethods(BindingFlags.Public | BindingFlags.Static) + 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); } - - public void RunValidators(CommandLineParser parser) - { - foreach (var validator in _type.GetCustomAttributes()) - { - validator.Validate(parser); - } - } } diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index ae149380..faab695e 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -1,6 +1,8 @@ // See https://aka.ms/new-console-template for more information using Ookii.CommandLine; +using Ookii.CommandLine.Conversion; using Ookii.CommandLine.Support; +using Ookii.CommandLine.Validation; var parser = new CommandLineParser(new MyProvider()); var arguments = (Arguments?)parser.ParseWithErrorHandling(); @@ -16,27 +18,23 @@ class Arguments public string? Test { get; set; } } -class MyProvider : IArgumentProvider +class MyProvider : GeneratedArgumentProvider { - public Type ArgumentsType => typeof(Arguments); - - public string ApplicationFriendlyName => "Test"; - - public string Description => string.Empty; - - public ParseOptionsAttribute? OptionsAttribute => null; + public MyProvider() + : base(typeof(Arguments), null, Enumerable.Empty(), null, null) + { + } - public bool IsCommand => false; + public override bool IsCommand => false; - public object CreateInstance(CommandLineParser parser) + public override object CreateInstance(CommandLineParser parser) { return new Arguments(); } - public IEnumerable GetArguments(CommandLineParser parser) - { - yield return CustomArgument.Create(parser, "Test", typeof(string), (target, value) => ((Arguments)target).Test = (string?)value); - } - public void RunValidators(CommandLineParser parser) + + public override IEnumerable GetArguments(CommandLineParser parser) { + yield return GeneratedArgument.Create(parser, typeof(string), "Test", new CommandLineArgumentAttribute(), + new StringConverter(), setProperty: (target, value) => ((Arguments)target).Test = (string?)value); } } From 5f1f335fd57495c2546b7228c980e2dd901e3a02 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 3 Apr 2023 18:34:06 -0700 Subject: [PATCH 012/234] Start on the source generator. --- .../AttributeNames.cs | 12 ++ .../Ookii.CommandLine.Generator.csproj | 20 +++ .../ParserGenerator.cs | 118 ++++++++++++++++++ .../ParserIncrementalGenerator.cs | 73 +++++++++++ .../SourceBuilder.cs | 60 +++++++++ .../SymbolExtensions.cs | 34 +++++ src/Ookii.CommandLine.sln | 8 +- .../CommandLineParserGeneric.cs | 31 ++++- .../GeneratedParserAttribute.cs | 12 ++ .../Properties/Resources.Designer.cs | 9 ++ .../Properties/Resources.resx | 3 + .../Support/ArgumentProvider.cs | 2 +- .../Support/GeneratedArgumentProvider.cs | 2 +- src/Samples/TrimTest/Program.cs | 31 ++--- src/Samples/TrimTest/TrimTest.csproj | 10 +- 15 files changed, 398 insertions(+), 27 deletions(-) create mode 100644 src/Ookii.CommandLine.Generator/AttributeNames.cs create mode 100644 src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj create mode 100644 src/Ookii.CommandLine.Generator/ParserGenerator.cs create mode 100644 src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs create mode 100644 src/Ookii.CommandLine.Generator/SourceBuilder.cs create mode 100644 src/Ookii.CommandLine.Generator/SymbolExtensions.cs create mode 100644 src/Ookii.CommandLine/GeneratedParserAttribute.cs diff --git a/src/Ookii.CommandLine.Generator/AttributeNames.cs b/src/Ookii.CommandLine.Generator/AttributeNames.cs new file mode 100644 index 00000000..bec554f5 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/AttributeNames.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ookii.CommandLine.Generator; + +internal static class AttributeNames +{ + public const string NamespacePrefix = "Ookii.CommandLine."; + public const string GeneratedParser = NamespacePrefix + "GeneratedParserAttribute"; + public const string CommandLineArgument = NamespacePrefix + "CommandLineArgumentAttribute"; +} diff --git a/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj b/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj new file mode 100644 index 00000000..f5919719 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + false + 11.0 + enable + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs new file mode 100644 index 00000000..c12c2e97 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -0,0 +1,118 @@ +using Microsoft.CodeAnalysis; +using System.Diagnostics; +using System.Text; + +namespace Ookii.CommandLine.Generator; + +internal static class ParserGenerator +{ + public static string Generate(SourceProductionContext context, INamedTypeSymbol symbol) + { + // TODO: Make sure it's a reference type and partial. + if (symbol.IsGenericType) + { + // TODO: Helper for reporting diagnostics. Maybe use exceptions? + // TODO: Use resources using LocalizableString + context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor("CL0001", "Generic arguments class", "The arguments class {0} may not be a generic class when the GeneratedParserAttribute is used.", "Ookii.CommandLine", DiagnosticSeverity.Error, true), symbol.Locations.FirstOrDefault(), symbol.ToDisplayString())); + return string.Empty; + } + + var builder = new SourceBuilder(symbol.ContainingNamespace); + builder.AppendLine($"partial class {symbol.Name}"); + builder.OpenBlock(); + GenerateProvider(builder, symbol); + builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{symbol.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new GeneratedProvider(), options);"); + builder.AppendLine(); + var nullableType = symbol.WithNullableAnnotation(NullableAnnotation.Annotated); + builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling();"); + builder.AppendLine(); + builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); + builder.AppendLine(); + builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, int index, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args, index);"); + builder.CloseBlock(); // class + return builder.GetSource(); + } + + private static void GenerateProvider(SourceBuilder builder, INamedTypeSymbol symbol) + { + builder.AppendLine("private class GeneratedProvider : Ookii.CommandLine.Support.GeneratedArgumentProvider"); + builder.OpenBlock(); + // TODO: attributes + builder.AppendLine($"public GeneratedProvider() : base(typeof({symbol.Name}), null, System.Linq.Enumerable.Empty(), null, null) {{}}"); + builder.AppendLine(); + // TODO: IsCommand + builder.AppendLine("public override bool IsCommand => false;"); + builder.AppendLine(); + // TODO: Injection + builder.AppendLine($"public override object CreateInstance(Ookii.CommandLine.CommandLineParser parser) => new {symbol.Name}();"); + builder.AppendLine(); + builder.AppendLine("public override System.Collections.Generic.IEnumerable GetArguments(Ookii.CommandLine.CommandLineParser parser)"); + builder.OpenBlock(); + + //Debugger.Launch(); + foreach (var member in symbol.GetMembers()) + { + GenerateArgument(builder, symbol.Name, member); + } + + // Makes sure the function compiles if there are no arguments. + builder.AppendLine("yield break;"); + builder.CloseBlock(); // GetArguments() + builder.CloseBlock(); // GeneratedProvider class + } + + private static void GenerateArgument(SourceBuilder builder, string className, ISymbol member) + { + // Check if the member can be an argument. + if (member.DeclaredAccessibility != Accessibility.Public || + member.Kind is not (SymbolKind.Method or SymbolKind.Property)) + { + return; + } + + AttributeData? commandLineArgumentAttribute = null; + foreach (var attribute in member.GetAttributes()) + { + if (attribute.AttributeClass == null) + { + continue; + } + + if (attribute.AttributeClass.DerivesFrom(AttributeNames.CommandLineArgument)) + { + commandLineArgumentAttribute = attribute; + } + } + + // Check if it is an attribute. + if (commandLineArgumentAttribute == null) + { + return; + } + + var property = member as IPropertySymbol; + var method = member as IMethodSymbol; + if (method != null) + { + throw new NotImplementedException(); + } + + var argumentType = (INamedTypeSymbol)property!.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + var nullableArgumentType = argumentType.WithNullableAnnotation(NullableAnnotation.Annotated); + string extra = string.Empty; + if (!argumentType.IsReferenceType && !argumentType.IsNullableValueType()) + { + extra = "!"; + } + + // 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.AppendLine(" parser"); + builder.AppendLine($" , argumentType: typeof({argumentType.ToDisplayString()})"); + builder.AppendLine($" , memberName: \"{member.Name}\""); + builder.AppendLine($" , attribute: {commandLineArgumentAttribute.CreateInstantiation()}"); + builder.AppendLine(" , converter: Ookii.CommandLine.Conversion.StringConverter.Instance"); + builder.AppendLine($" , setProperty: (target, value) => (({className})target).{member.Name} = ({nullableArgumentType.ToDisplayString()})value{extra}"); + builder.AppendLine($");"); + } +} diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs new file mode 100644 index 00000000..53131f6f --- /dev/null +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -0,0 +1,73 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Text; + +namespace Ookii.CommandLine.Generator; + +[Generator] +public class ParserIncrementalGenerator : IIncrementalGenerator +{ + 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; + } + + foreach (var syntax in classes) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + var semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(syntax, context.CancellationToken) is not INamedTypeSymbol symbol) + { + continue; + } + + var source = ParserGenerator.Generate(context, symbol); + context.AddSource(symbol.Name + ".g.cs", SourceText.From(source, Encoding.UTF8)); + } + } + + private static ClassDeclarationSyntax? GetClassToGenerate(GeneratorSyntaxContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + 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; + var name = attributeType.ToDisplayString(); + if (name == AttributeNames.GeneratedParser) + { + return classDeclaration; + } + } + } + + return null; + } +} diff --git a/src/Ookii.CommandLine.Generator/SourceBuilder.cs b/src/Ookii.CommandLine.Generator/SourceBuilder.cs new file mode 100644 index 00000000..6e9ff3b9 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/SourceBuilder.cs @@ -0,0 +1,60 @@ +using Microsoft.CodeAnalysis; +using System.Text; + +namespace Ookii.CommandLine.Generator; + +internal class SourceBuilder +{ + private readonly StringBuilder _builder = new(); + private int _indentLevel; + + public SourceBuilder(INamespaceSymbol ns) + { + _builder.AppendLine("#nullable enable"); + _builder.AppendLine(); + if (!ns.IsGlobalNamespace) + { + AppendLine($"namespace {ns.ToDisplayString()}"); + OpenBlock(); + } + } + + public void AppendLine() + { + _builder.AppendLine(); + } + + public void AppendLine(string text) + { + WriteIndent(); + _builder.AppendLine(text); + } + + public void OpenBlock() + { + AppendLine("{"); + ++_indentLevel; + } + + public void CloseBlock() + { + --_indentLevel; + AppendLine("}"); + } + + public string GetSource() + { + while (_indentLevel > 0) + { + CloseBlock(); + } + + return _builder.ToString(); + } + + private void WriteIndent() + { + _builder.Append(' ', _indentLevel * 4); + } + +} diff --git a/src/Ookii.CommandLine.Generator/SymbolExtensions.cs b/src/Ookii.CommandLine.Generator/SymbolExtensions.cs new file mode 100644 index 00000000..867524ea --- /dev/null +++ b/src/Ookii.CommandLine.Generator/SymbolExtensions.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Text; + +namespace Ookii.CommandLine.Generator; + +internal static class SymbolExtensions +{ + public static bool DerivesFrom(this INamedTypeSymbol symbol, string baseClassName) + { + INamedTypeSymbol? current = symbol; + while (current != null) + { + if (current.ToDisplayString() == baseClassName) + { + return true; + } + + current = current.BaseType; + } + + return false; + } + + public static bool IsNullableValueType(this INamedTypeSymbol symbol) + => !symbol.IsReferenceType && symbol.IsGenericType && symbol.ConstructedFrom.ToDisplayString() == "System.Nullable"; + + public static string CreateInstantiation(this AttributeData attribute) + { + var ctorArgs = attribute.ConstructorArguments.Select(c => c.ToCSharpString()); + var namedArgs = attribute.NamedArguments.Select(n => $"{n.Key} = {n.Value.ToCSharpString()}"); + return $"new {attribute.AttributeClass?.ToDisplayString()}({string.Join(", ", ctorArgs)}) {{ {string.Join(", ", namedArgs)} }}"; + } +} diff --git a/src/Ookii.CommandLine.sln b/src/Ookii.CommandLine.sln index be838c12..361254ef 100644 --- a/src/Ookii.CommandLine.sln +++ b/src/Ookii.CommandLine.sln @@ -32,7 +32,9 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrimTest", "Samples\TrimTest\TrimTest.csproj", "{D3422CDA-6FAF-4EED-9CB1-814A0D519373}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TrimTest", "Samples\TrimTest\TrimTest.csproj", "{D3422CDA-6FAF-4EED-9CB1-814A0D519373}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ookii.CommandLine.Generator", "Ookii.CommandLine.Generator\Ookii.CommandLine.Generator.csproj", "{9C027C37-4BEA-422F-A148-1F73C6FFEF45}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -80,6 +82,10 @@ Global {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index edea07e5..46e37a93 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -1,4 +1,6 @@ -using System; +using Ookii.CommandLine.Support; +using System; +using System.Globalization; namespace Ookii.CommandLine { @@ -42,6 +44,33 @@ public CommandLineParser(ParseOptions? options = null) { } + /// + /// 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(ArgumentProvider provider, ParseOptions? options = null) + : base(provider, options) + { + if (provider.ArgumentsType != typeof(T)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.IncorrectProviderTypeFormat, typeof(T)), nameof(provider)); + } + } + /// public new T? Parse() { diff --git a/src/Ookii.CommandLine/GeneratedParserAttribute.cs b/src/Ookii.CommandLine/GeneratedParserAttribute.cs new file mode 100644 index 00000000..ec8e2379 --- /dev/null +++ b/src/Ookii.CommandLine/GeneratedParserAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Ookii.CommandLine; + +/// +/// Indicates that the specified arguments type should use source generation. +/// TODO: Better help. +/// +[AttributeUsage(AttributeTargets.Class)] +public class GeneratedParserAttribute : Attribute +{ +} diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 2d6edf11..dfe452de 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -312,6 +312,15 @@ internal static string EmptyKeyValueSeparator { } } + /// + /// 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.. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index 46f4c726..0ac69b36 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -393,4 +393,7 @@ An async write operation is already in progress. + + The provided ArgumentProvider is not for the type '{0}'. + \ No newline at end of file diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs index 27578815..ae82d06a 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -9,7 +9,7 @@ namespace Ookii.CommandLine.Support; /// A source of arguments for the . /// /// -/// This interface is used by the source generator when using +/// This interface is used by the source generator when using /// attribute. It should not normally be used by regular code. /// public abstract class ArgumentProvider diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs index 2880175d..c3d14848 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -9,7 +9,7 @@ namespace Ookii.CommandLine.Support; /// -/// A base class for argument providers created by the . +/// A base class for argument providers created by the . /// /// /// This type is for internal use and should not be used by your code. diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index faab695e..1cfb2d69 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -4,37 +4,24 @@ using Ookii.CommandLine.Support; using Ookii.CommandLine.Validation; -var parser = new CommandLineParser(new MyProvider()); -var arguments = (Arguments?)parser.ParseWithErrorHandling(); +var arguments = Arguments.Parse(); if (arguments != null) { Console.WriteLine($"Hello, World! {arguments.Test}"); } - -class Arguments +[GeneratedParser] +partial class Arguments { [CommandLineArgument] public string? Test { get; set; } -} - -class MyProvider : GeneratedArgumentProvider -{ - public MyProvider() - : base(typeof(Arguments), null, Enumerable.Empty(), null, null) - { - } - public override bool IsCommand => false; + [CommandLineArgument(ValueDescription = "Stuff")] + public Dictionary? Test2 { get; set; } = default!; - public override object CreateInstance(CommandLineParser parser) - { - return new Arguments(); - } + [CommandLineArgument] + public int Test3 { get; set; } - public override IEnumerable GetArguments(CommandLineParser parser) - { - yield return GeneratedArgument.Create(parser, typeof(string), "Test", new CommandLineArgumentAttribute(), - new StringConverter(), setProperty: (target, value) => ((Arguments)target).Test = (string?)value); - } + [CommandLineArgument] + public int? Test4 { get; set; } } diff --git a/src/Samples/TrimTest/TrimTest.csproj b/src/Samples/TrimTest/TrimTest.csproj index 8b1b3f47..6a070ee7 100644 --- a/src/Samples/TrimTest/TrimTest.csproj +++ b/src/Samples/TrimTest/TrimTest.csproj @@ -1,4 +1,4 @@ - + Exe @@ -7,10 +7,18 @@ enable true true + true + + + + + From 148c74b963d6b4618e38f9dcf1e54b58b533ca8d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 4 Apr 2023 18:04:17 -0700 Subject: [PATCH 013/234] Include generator in nuget package. --- src/Directory.Build.props | 3 ++- src/Ookii.CommandLine/Ookii.CommandLine.csproj | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 42ecd03a..06c3b98e 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,6 +4,7 @@ Sven Groot Ookii.org Copyright (c) Sven Groot (Ookii.org) - 3.1.1 + 4.0.0 + preview \ No newline at end of file diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index 828845dc..8dbb3074 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -33,15 +33,20 @@ CS1574 + + + + + + - True From cd5f6ae0e0ea138ae3c82b513fd81d80ca9f134e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 4 Apr 2023 18:33:01 -0700 Subject: [PATCH 014/234] Use resources for diagnostics. --- .../Diagnostics.cs | 18 +++ .../Ookii.CommandLine.Generator.csproj | 15 +++ .../ParserGenerator.cs | 5 +- .../Properties/Resources.Designer.cs | 81 +++++++++++ .../Properties/Resources.resx | 126 ++++++++++++++++++ 5 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 src/Ookii.CommandLine.Generator/Diagnostics.cs create mode 100644 src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs create mode 100644 src/Ookii.CommandLine.Generator/Properties/Resources.resx diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs new file mode 100644 index 00000000..272b9b65 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; +using Ookii.CommandLine.Generator.Properties; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ookii.CommandLine.Generator; + +internal static class Diagnostics +{ + public static DiagnosticDescriptor ArgumentsClassIsGeneric => new( + "CL1001", + new LocalizableResourceString(nameof(Resources.ArgumentsClassIsGenericTitle), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.ArgumentsClassIsGenericMessageFormat), Resources.ResourceManager, typeof(Resources)), + "Ookii.CommandLine", + DiagnosticSeverity.Error, + isEnabledByDefault: true); +} diff --git a/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj b/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj index f5919719..7d934245 100644 --- a/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj +++ b/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj @@ -17,4 +17,19 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index c12c2e97..e1b43564 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -11,9 +11,8 @@ public static string Generate(SourceProductionContext context, INamedTypeSymbol // TODO: Make sure it's a reference type and partial. if (symbol.IsGenericType) { - // TODO: Helper for reporting diagnostics. Maybe use exceptions? - // TODO: Use resources using LocalizableString - context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor("CL0001", "Generic arguments class", "The arguments class {0} may not be a generic class when the GeneratedParserAttribute is used.", "Ookii.CommandLine", DiagnosticSeverity.Error, true), symbol.Locations.FirstOrDefault(), symbol.ToDisplayString())); + // TODO: Helper for reporting diagnostics. + context.ReportDiagnostic(Diagnostic.Create(Diagnostics.ArgumentsClassIsGeneric, symbol.Locations.FirstOrDefault(), symbol.ToDisplayString())); return string.Empty; } 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..6a1246c1 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// 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 arguments class {0} may not be a generic class when the GeneratedParserAttribute is used.. + /// + internal static string ArgumentsClassIsGenericMessageFormat { + get { + return ResourceManager.GetString("ArgumentsClassIsGenericMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The arguments class may not be a generic type.. + /// + internal static string ArgumentsClassIsGenericTitle { + get { + return ResourceManager.GetString("ArgumentsClassIsGenericTitle", 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..88cff17a --- /dev/null +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 arguments class {0} may not be a generic class when the GeneratedParserAttribute is used. + + + The arguments class may not be a generic type. + + \ No newline at end of file From 092471b0f52bf4a8c9f2960fdfd3821b9d2e5e37 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 5 Apr 2023 16:46:03 -0700 Subject: [PATCH 015/234] Process arguments class attributes. --- .../AttributeNames.cs | 6 + .../Diagnostics.cs | 48 +++++- .../ParserGenerator.cs | 161 ++++++++++++------ .../ParserIncrementalGenerator.cs | 24 ++- .../Properties/Resources.Designer.cs | 54 ++++++ .../Properties/Resources.resx | 18 ++ .../SourceBuilder.cs | 6 +- .../Support/ArgumentProvider.cs | 5 +- .../Support/GeneratedArgumentProvider.cs | 2 +- src/Samples/TrimTest/Program.cs | 5 + 10 files changed, 268 insertions(+), 61 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/AttributeNames.cs b/src/Ookii.CommandLine.Generator/AttributeNames.cs index bec554f5..d7a0d899 100644 --- a/src/Ookii.CommandLine.Generator/AttributeNames.cs +++ b/src/Ookii.CommandLine.Generator/AttributeNames.cs @@ -9,4 +9,10 @@ internal static class AttributeNames public const string NamespacePrefix = "Ookii.CommandLine."; public const string GeneratedParser = NamespacePrefix + "GeneratedParserAttribute"; public const string CommandLineArgument = NamespacePrefix + "CommandLineArgumentAttribute"; + public const string ParseOptions = NamespacePrefix + "ParseOptionsAttribute"; + public const string ApplicationFriendlyName = NamespacePrefix + "ApplicationFriendlyNameAttribute"; + public const string Command = NamespacePrefix + "Commands.CommandAttribute"; + public const string ClassValidation = NamespacePrefix + "Validation.ClassValidationAttribute"; + + public const string Description = "System.ComponentModel.DescriptionAttribute"; } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 272b9b65..153355db 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -8,11 +8,45 @@ namespace Ookii.CommandLine.Generator; internal static class Diagnostics { - public static DiagnosticDescriptor ArgumentsClassIsGeneric => new( - "CL1001", - new LocalizableResourceString(nameof(Resources.ArgumentsClassIsGenericTitle), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.ArgumentsClassIsGenericMessageFormat), Resources.ResourceManager, typeof(Resources)), - "Ookii.CommandLine", - DiagnosticSeverity.Error, - isEnabledByDefault: true); + private const string Category = "Ookii.CommandLine"; + + public static Diagnostic ArgumentsTypeNotReferenceType(INamedTypeSymbol symbol) => Diagnostic.Create( + new DiagnosticDescriptor( + "CL1001", + new LocalizableResourceString(nameof(Resources.ArgumentsTypeNotReferenceTypeTitle), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.ArgumentsTypeNotReferenceTypeMessageFormat), Resources.ResourceManager, typeof(Resources)), + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true), + symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + + public static Diagnostic ArgumentsClassNotPartial(INamedTypeSymbol symbol) => Diagnostic.Create( + new DiagnosticDescriptor( + "CL1002", + new LocalizableResourceString(nameof(Resources.ArgumentsClassNotPartialTitle), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.ArgumentsClassNotPartialMessageFormat), Resources.ResourceManager, typeof(Resources)), + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true), + symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + + public static Diagnostic ArgumentsClassIsGeneric(INamedTypeSymbol symbol) => Diagnostic.Create( + new DiagnosticDescriptor( + "CL1003", + new LocalizableResourceString(nameof(Resources.ArgumentsClassIsGenericTitle), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.ArgumentsClassIsGenericMessageFormat), Resources.ResourceManager, typeof(Resources)), + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true), + symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + + public static Diagnostic UnknownAttribute(AttributeData attribute) => Diagnostic.Create( + new DiagnosticDescriptor( + "CLW1001", + new LocalizableResourceString(nameof(Resources.UnknownAttributeTitle), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.UnknownAttributeMessageFormat), Resources.ResourceManager, typeof(Resources)), + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true), + attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), attribute.AttributeClass?.Name); } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index e1b43564..2258fa02 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -4,63 +4,114 @@ namespace Ookii.CommandLine.Generator; -internal static class ParserGenerator +internal class ParserGenerator { - public static string Generate(SourceProductionContext context, INamedTypeSymbol symbol) + private readonly SourceProductionContext _context; + private readonly INamedTypeSymbol _argumentsClass; + private readonly SourceBuilder _builder; + + public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumentsClass) { - // TODO: Make sure it's a reference type and partial. - if (symbol.IsGenericType) - { - // TODO: Helper for reporting diagnostics. - context.ReportDiagnostic(Diagnostic.Create(Diagnostics.ArgumentsClassIsGeneric, symbol.Locations.FirstOrDefault(), symbol.ToDisplayString())); - return string.Empty; - } + _context = context; + _argumentsClass = argumentsClass; + _builder = new(argumentsClass.ContainingNamespace); + } - var builder = new SourceBuilder(symbol.ContainingNamespace); - builder.AppendLine($"partial class {symbol.Name}"); - builder.OpenBlock(); - GenerateProvider(builder, symbol); - builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{symbol.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new GeneratedProvider(), options);"); - builder.AppendLine(); - var nullableType = symbol.WithNullableAnnotation(NullableAnnotation.Annotated); - builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling();"); - builder.AppendLine(); - builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); - builder.AppendLine(); - builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, int index, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args, index);"); - builder.CloseBlock(); // class - return builder.GetSource(); + public static string? Generate(SourceProductionContext context, INamedTypeSymbol _argumentsClass) + { + var generator = new ParserGenerator(context, _argumentsClass); + return generator.Generate(); + } + + public string? Generate() + { + _builder.AppendLine($"partial class {_argumentsClass.Name}"); + _builder.OpenBlock(); + GenerateProvider(); + _builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{_argumentsClass.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new GeneratedProvider(), options);"); + _builder.AppendLine(); + var nullableType = _argumentsClass.WithNullableAnnotation(NullableAnnotation.Annotated); + _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling();"); + _builder.AppendLine(); + _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); + _builder.AppendLine(); + _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, int index, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args, index);"); + _builder.CloseBlock(); // class + return _builder.GetSource(); } - private static void GenerateProvider(SourceBuilder builder, INamedTypeSymbol symbol) + private void GenerateProvider() { - builder.AppendLine("private class GeneratedProvider : Ookii.CommandLine.Support.GeneratedArgumentProvider"); - builder.OpenBlock(); - // TODO: attributes - builder.AppendLine($"public GeneratedProvider() : base(typeof({symbol.Name}), null, System.Linq.Enumerable.Empty(), null, null) {{}}"); - builder.AppendLine(); + AttributeData? parseOptions = null; + AttributeData? description = null; + AttributeData? applicationFriendlyName = null; + AttributeData? commandAttribute = null; + List? classValidators = null; + foreach (var attribute in _argumentsClass.GetAttributes()) + { + if (CheckAttribute(attribute, AttributeNames.ParseOptions, ref parseOptions) || + CheckAttribute(attribute, AttributeNames.Description, ref description) || + CheckAttribute(attribute, AttributeNames.ApplicationFriendlyName, ref applicationFriendlyName) || + CheckAttribute(attribute, AttributeNames.Command, ref commandAttribute)) + { + continue; + } + + if (attribute.AttributeClass?.DerivesFrom(AttributeNames.ClassValidation) ?? false) + { + classValidators ??= new(); + classValidators.Add(attribute); + continue; + } + + if (!attribute.AttributeClass?.DerivesFrom(AttributeNames.GeneratedParser) ?? false) + { + _context.ReportDiagnostic(Diagnostics.UnknownAttribute(attribute)); + } + } + + _builder.AppendLine("private class GeneratedProvider : Ookii.CommandLine.Support.GeneratedArgumentProvider"); + _builder.OpenBlock(); + _builder.AppendLine("public GeneratedProvider()"); + _builder.IncreaseIndent(); + _builder.AppendLine($": base(typeof({_argumentsClass.Name}),"); + _builder.AppendLine($" {parseOptions?.CreateInstantiation() ?? "null"},"); + if (classValidators == null) + { + _builder.AppendLine($" null,"); + } + else + { + _builder.AppendLine($" new Ookii.CommandLine.Validation.ClassValidationAttribute[] {{ {string.Join(", ", classValidators.Select(v => v.CreateInstantiation()))} }},"); + } + + _builder.AppendLine($" {applicationFriendlyName?.CreateInstantiation() ?? "null"},"); + _builder.AppendLine($" {description?.CreateInstantiation() ?? "null"})"); + _builder.DecreaseIndent(); + _builder.AppendLine("{}"); + _builder.AppendLine(); // TODO: IsCommand - builder.AppendLine("public override bool IsCommand => false;"); - builder.AppendLine(); + _builder.AppendLine("public override bool IsCommand => false;"); + _builder.AppendLine(); // TODO: Injection - builder.AppendLine($"public override object CreateInstance(Ookii.CommandLine.CommandLineParser parser) => new {symbol.Name}();"); - builder.AppendLine(); - builder.AppendLine("public override System.Collections.Generic.IEnumerable GetArguments(Ookii.CommandLine.CommandLineParser parser)"); - builder.OpenBlock(); + _builder.AppendLine($"public override object CreateInstance(Ookii.CommandLine.CommandLineParser parser) => new {_argumentsClass.Name}();"); + _builder.AppendLine(); + _builder.AppendLine("public override System.Collections.Generic.IEnumerable GetArguments(Ookii.CommandLine.CommandLineParser parser)"); + _builder.OpenBlock(); //Debugger.Launch(); - foreach (var member in symbol.GetMembers()) + foreach (var member in _argumentsClass.GetMembers()) { - GenerateArgument(builder, symbol.Name, member); + GenerateArgument(member); } // Makes sure the function compiles if there are no arguments. - builder.AppendLine("yield break;"); - builder.CloseBlock(); // GetArguments() - builder.CloseBlock(); // GeneratedProvider class + _builder.AppendLine("yield break;"); + _builder.CloseBlock(); // GetArguments() + _builder.CloseBlock(); // GeneratedProvider class } - private static void GenerateArgument(SourceBuilder builder, string className, ISymbol member) + private void GenerateArgument(ISymbol member) { // Check if the member can be an argument. if (member.DeclaredAccessibility != Accessibility.Public || @@ -105,13 +156,25 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) } // 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.AppendLine(" parser"); - builder.AppendLine($" , argumentType: typeof({argumentType.ToDisplayString()})"); - builder.AppendLine($" , memberName: \"{member.Name}\""); - builder.AppendLine($" , attribute: {commandLineArgumentAttribute.CreateInstantiation()}"); - builder.AppendLine(" , converter: Ookii.CommandLine.Conversion.StringConverter.Instance"); - builder.AppendLine($" , setProperty: (target, value) => (({className})target).{member.Name} = ({nullableArgumentType.ToDisplayString()})value{extra}"); - builder.AppendLine($");"); + _builder.AppendLine($"yield return Ookii.CommandLine.Support.GeneratedArgument.Create("); + _builder.AppendLine(" parser"); + _builder.AppendLine($" , argumentType: typeof({argumentType.ToDisplayString()})"); + _builder.AppendLine($" , memberName: \"{member.Name}\""); + _builder.AppendLine($" , attribute: {commandLineArgumentAttribute.CreateInstantiation()}"); + _builder.AppendLine(" , converter: Ookii.CommandLine.Conversion.StringConverter.Instance"); + _builder.AppendLine($" , setProperty: (target, value) => (({_argumentsClass.Name})target).{member.Name} = ({nullableArgumentType.ToDisplayString()})value{extra}"); + _builder.AppendLine($");"); + } + + // Using a ref parameter with bool return allows me to chain these together. + private static bool CheckAttribute(AttributeData data, string name, ref AttributeData? attribute) + { + if (attribute != null || !(data.AttributeClass?.DerivesFrom(name) ?? false)) + { + return false; + } + + attribute = data; + return true; } } diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index 53131f6f..57703841 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using System.Collections.Immutable; @@ -41,8 +42,29 @@ private static void Execute(Compilation compilation, ImmutableArray m.IsKind(SyntaxKind.PartialKeyword))) + { + context.ReportDiagnostic(Diagnostics.ArgumentsClassNotPartial(symbol)); + continue; + } + + if (symbol.IsGenericType) + { + context.ReportDiagnostic(Diagnostics.ArgumentsClassIsGeneric(symbol)); + continue; + } + var source = ParserGenerator.Generate(context, symbol); - context.AddSource(symbol.Name + ".g.cs", SourceText.From(source, Encoding.UTF8)); + if (source != null) + { + context.AddSource(symbol.Name + ".g.cs", SourceText.From(source, Encoding.UTF8)); + } } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 6a1246c1..ca357121 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -77,5 +77,59 @@ internal static string ArgumentsClassIsGenericTitle { return ResourceManager.GetString("ArgumentsClassIsGenericTitle", resourceCulture); } } + + /// + /// Looks up a localized string similar to The arguments class {0} must use the 'partial' modifier.. + /// + internal static string ArgumentsClassNotPartialMessageFormat { + get { + return ResourceManager.GetString("ArgumentsClassNotPartialMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The arguments class must be a partial class.. + /// + internal static string ArgumentsClassNotPartialTitle { + get { + return ResourceManager.GetString("ArgumentsClassNotPartialTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The arguments type {0} must be a reference type (class).. + /// + internal static string ArgumentsTypeNotReferenceTypeMessageFormat { + get { + return ResourceManager.GetString("ArgumentsTypeNotReferenceTypeMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The arguments type must be a reference type.. + /// + internal static string ArgumentsTypeNotReferenceTypeTitle { + get { + return ResourceManager.GetString("ArgumentsTypeNotReferenceTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The attribute {0} is unknown and will be ignored by the GeneratedParserAttribute.. + /// + internal static string UnknownAttributeMessageFormat { + get { + return ResourceManager.GetString("UnknownAttributeMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown attribute will be ignored.. + /// + internal static string UnknownAttributeTitle { + get { + return ResourceManager.GetString("UnknownAttributeTitle", resourceCulture); + } + } } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 88cff17a..0a38a606 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -123,4 +123,22 @@ The arguments class may not be a generic type. + + The arguments class {0} must use the 'partial' modifier. + + + The arguments class must be a partial class. + + + The arguments type {0} must be a reference type (class). + + + The arguments type must be a reference type. + + + The attribute {0} is unknown and will be ignored by the GeneratedParserAttribute. + + + Unknown attribute will be ignored. + \ No newline at end of file diff --git a/src/Ookii.CommandLine.Generator/SourceBuilder.cs b/src/Ookii.CommandLine.Generator/SourceBuilder.cs index 6e9ff3b9..2f1c8b5e 100644 --- a/src/Ookii.CommandLine.Generator/SourceBuilder.cs +++ b/src/Ookii.CommandLine.Generator/SourceBuilder.cs @@ -10,6 +10,7 @@ internal class SourceBuilder public SourceBuilder(INamespaceSymbol ns) { + _builder.AppendLine("// "); _builder.AppendLine("#nullable enable"); _builder.AppendLine(); if (!ns.IsGlobalNamespace) @@ -52,9 +53,12 @@ public string GetSource() return _builder.ToString(); } + public void IncreaseIndent() => ++_indentLevel; + + public void DecreaseIndent() => --_indentLevel; + private void WriteIndent() { _builder.Append(' ', _indentLevel * 4); } - } diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs index ae82d06a..08c6738e 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; namespace Ookii.CommandLine.Support; @@ -25,11 +26,11 @@ public abstract class ArgumentProvider /// if there is none. /// /// The class validators for the arguments type. - protected ArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, IEnumerable validators) + protected ArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, IEnumerable? validators) { ArgumentsType = argumentsType ?? throw new ArgumentNullException(nameof(argumentsType)); OptionsAttribute = options; - _validators = validators ?? throw new ArgumentNullException(nameof(validators)); + _validators = validators ?? Enumerable.Empty(); } /// diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs index c3d14848..84662345 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -37,7 +37,7 @@ public abstract class GeneratedArgumentProvider : ArgumentProvider /// there is none. /// protected GeneratedArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, - IEnumerable validators, ApplicationFriendlyNameAttribute? friendlyName, + IEnumerable? validators, ApplicationFriendlyNameAttribute? friendlyName, DescriptionAttribute? description) : base(argumentsType, options, validators) { diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 1cfb2d69..ff4492c3 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -3,6 +3,7 @@ using Ookii.CommandLine.Conversion; using Ookii.CommandLine.Support; using Ookii.CommandLine.Validation; +using System.ComponentModel; var arguments = Arguments.Parse(); if (arguments != null) @@ -11,6 +12,10 @@ } [GeneratedParser] +[ParseOptions(Mode = ParsingMode.LongShort, CaseSensitive = true)] +[Description("This is a test")] +[ApplicationFriendlyName("Trim Test")] +[RequiresAny(nameof(Test), nameof(Test2))] partial class Arguments { [CommandLineArgument] From 2a64f284f37825cae71e6b36ad1db12d1676ceeb Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Apr 2023 10:21:55 -0700 Subject: [PATCH 016/234] Unseal CommandLineArgumentAttribute. --- src/Ookii.CommandLine/CommandLineArgumentAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index 2f8d359e..4341e7ba 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -56,7 +56,7 @@ namespace Ookii.CommandLine /// /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] - public sealed class CommandLineArgumentAttribute : Attribute + public class CommandLineArgumentAttribute : Attribute { private readonly string? _argumentName; private bool _short; From 7b88622e9e9ad426328e38ea1f61f4f41c9979e2 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Apr 2023 16:47:52 -0700 Subject: [PATCH 017/234] Argument attributes support. --- .../AttributeNames.cs | 7 ++ .../ParserGenerator.cs | 103 ++++++++++++++---- .../Support/GeneratedArgument.cs | 2 +- src/Samples/TrimTest/Program.cs | 8 +- 4 files changed, 99 insertions(+), 21 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/AttributeNames.cs b/src/Ookii.CommandLine.Generator/AttributeNames.cs index d7a0d899..da7cb0d5 100644 --- a/src/Ookii.CommandLine.Generator/AttributeNames.cs +++ b/src/Ookii.CommandLine.Generator/AttributeNames.cs @@ -13,6 +13,13 @@ internal static class AttributeNames public const string ApplicationFriendlyName = NamespacePrefix + "ApplicationFriendlyNameAttribute"; public const string Command = NamespacePrefix + "Commands.CommandAttribute"; public const string ClassValidation = NamespacePrefix + "Validation.ClassValidationAttribute"; + public const string MultiValueSeparator = NamespacePrefix + "MultiValueSeparatorAttribute"; + public const string KeyValueSeparator = NamespacePrefix + "Conversion.KeyValueSeparatorAttribute"; + public const string AllowDuplicateDictionaryKeys = NamespacePrefix + "AllowDuplicateDictionaryKeysAttribute"; + public const string Alias = NamespacePrefix + "AliasAttribute"; + public const string ShortAlias = NamespacePrefix + "ShortAliasAttribute"; + public const string ArgumentValidation = NamespacePrefix + "Validation.ArgumentValidationAttribute"; + public const string ArgumentConverter = NamespacePrefix + "Conversion.ArgumentConverterAttribute"; public const string Description = "System.ComponentModel.DescriptionAttribute"; } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 2258fa02..da04a033 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using System; using System.Diagnostics; using System.Text; @@ -42,6 +43,10 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen private void GenerateProvider() { + // Find the attribute 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. AttributeData? parseOptions = null; AttributeData? description = null; AttributeData? applicationFriendlyName = null; @@ -52,18 +57,12 @@ private void GenerateProvider() if (CheckAttribute(attribute, AttributeNames.ParseOptions, ref parseOptions) || CheckAttribute(attribute, AttributeNames.Description, ref description) || CheckAttribute(attribute, AttributeNames.ApplicationFriendlyName, ref applicationFriendlyName) || - CheckAttribute(attribute, AttributeNames.Command, ref commandAttribute)) + CheckAttribute(attribute, AttributeNames.Command, ref commandAttribute) || + CheckAttribute(attribute, AttributeNames.ClassValidation, ref classValidators)) { continue; } - if (attribute.AttributeClass?.DerivesFrom(AttributeNames.ClassValidation) ?? false) - { - classValidators ??= new(); - classValidators.Add(attribute); - continue; - } - if (!attribute.AttributeClass?.DerivesFrom(AttributeNames.GeneratedParser) ?? false) { _context.ReportDiagnostic(Diagnostics.UnknownAttribute(attribute)); @@ -121,17 +120,30 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) } AttributeData? commandLineArgumentAttribute = null; + AttributeData? multiValueSeparator = null; + AttributeData? description = null; + AttributeData? allowDuplicateDictionaryKeys = null; + AttributeData? keyValueSeparator = null; + AttributeData? converter = null; + List? aliases = null; + List? shortAliases = null; + List? validators = null; foreach (var attribute in member.GetAttributes()) { - if (attribute.AttributeClass == null) + if (CheckAttribute(attribute, AttributeNames.CommandLineArgument, ref commandLineArgumentAttribute) || + CheckAttribute(attribute, AttributeNames.MultiValueSeparator, ref multiValueSeparator) || + CheckAttribute(attribute, AttributeNames.Description, ref description) || + CheckAttribute(attribute, AttributeNames.AllowDuplicateDictionaryKeys, ref allowDuplicateDictionaryKeys) || + CheckAttribute(attribute, AttributeNames.KeyValueSeparator, ref keyValueSeparator) || + CheckAttribute(attribute, AttributeNames.ArgumentConverter, ref converter) || + CheckAttribute(attribute, AttributeNames.Alias, ref aliases) || + CheckAttribute(attribute, AttributeNames.ShortAlias, ref shortAliases) || + CheckAttribute(attribute, AttributeNames.ArgumentValidation, ref validators)) { continue; } - if (attribute.AttributeClass.DerivesFrom(AttributeNames.CommandLineArgument)) - { - commandLineArgumentAttribute = attribute; - } + _context.ReportDiagnostic(Diagnostics.UnknownAttribute(attribute)); } // Check if it is an attribute. @@ -155,14 +167,54 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) extra = "!"; } + // TODO: key/value converters + // TODO: AllowsNull + // 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.AppendLine(" parser"); - _builder.AppendLine($" , argumentType: typeof({argumentType.ToDisplayString()})"); - _builder.AppendLine($" , memberName: \"{member.Name}\""); - _builder.AppendLine($" , attribute: {commandLineArgumentAttribute.CreateInstantiation()}"); - _builder.AppendLine(" , converter: Ookii.CommandLine.Conversion.StringConverter.Instance"); - _builder.AppendLine($" , setProperty: (target, value) => (({_argumentsClass.Name})target).{member.Name} = ({nullableArgumentType.ToDisplayString()})value{extra}"); + _builder.IncreaseIndent(); + _builder.AppendLine("parser"); + _builder.AppendLine($", argumentType: typeof({argumentType.ToDisplayString()})"); + _builder.AppendLine($", memberName: \"{member.Name}\""); + _builder.AppendLine($", attribute: {commandLineArgumentAttribute.CreateInstantiation()}"); + _builder.AppendLine(", converter: Ookii.CommandLine.Conversion.StringConverter.Instance"); + if (multiValueSeparator != null) + { + _builder.AppendLine($", multiValueSeparatorAttribute: {multiValueSeparator.CreateInstantiation()}"); + } + + if (description != null) + { + _builder.AppendLine($", descriptionAttribute: {description.CreateInstantiation()}"); + } + + if (allowDuplicateDictionaryKeys != null) + { + _builder.AppendLine(", allowDuplicateDictionaryKeys: true"); + } + + if (keyValueSeparator != null) + { + _builder.AppendLine($", keyValueSeparatorAttribute: {keyValueSeparator.CreateInstantiation()}"); + } + + if (aliases != null) + { + _builder.AppendLine($", aliasAttributes: new Ookii.CommandLine.AliasAttribute[] {{ {string.Join(", ", aliases.Select(a => a.CreateInstantiation()))} }}"); + } + + if (shortAliases != null) + { + _builder.AppendLine($", shortAliasAttributes: new Ookii.CommandLine.ShortAliasAttribute[] {{ {string.Join(", ", shortAliases.Select(a => a.CreateInstantiation()))} }}"); + } + + if (validators != null) + { + _builder.AppendLine($", validationAttributes: new Ookii.CommandLine.Validation.ArgumentValidationAttribute[] {{ {string.Join(", ", validators.Select(a => a.CreateInstantiation()))} }}"); + } + + _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.Name})target).{member.Name} = ({nullableArgumentType.ToDisplayString()})value{extra}"); + _builder.DecreaseIndent(); _builder.AppendLine($");"); } @@ -177,4 +229,17 @@ private static bool CheckAttribute(AttributeData data, string name, ref Attribut attribute = data; return true; } + + // Using a ref parameter with bool return allows me to chain these together. + private static bool CheckAttribute(AttributeData data, string name, ref List? attributes) + { + if (!(data.AttributeClass?.DerivesFrom(name) ?? false)) + { + return false; + } + + attributes ??= new(); + attributes.Add(data); + return true; + } } diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 9d8efa01..728088a9 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -58,7 +58,7 @@ public static GeneratedArgument Create(CommandLineParser parser, multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); - // TODO: Set property for multi-value and Nullable. + // TODO: Set properly for multi-value and Nullable. info.ElementType = argumentType; info.Converter = converter; diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index ff4492c3..a9d59e31 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -4,6 +4,7 @@ using Ookii.CommandLine.Support; using Ookii.CommandLine.Validation; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; var arguments = Arguments.Parse(); if (arguments != null) @@ -12,16 +13,21 @@ } [GeneratedParser] -[ParseOptions(Mode = ParsingMode.LongShort, CaseSensitive = true)] +[ParseOptions(CaseSensitive = true)] [Description("This is a test")] [ApplicationFriendlyName("Trim Test")] [RequiresAny(nameof(Test), nameof(Test2))] partial class Arguments { [CommandLineArgument] + [Description("Test argument")] + [Alias("t")] + [ValidateNotEmpty] public string? Test { get; set; } [CommandLineArgument(ValueDescription = "Stuff")] + [KeyValueSeparator("==")] + [MultiValueSeparator] public Dictionary? Test2 { get; set; } = default!; [CommandLineArgument] From 26dde15c82b889c02d4cd530d41b021d8a4efb91 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Sun, 9 Apr 2023 17:05:31 -0700 Subject: [PATCH 018/234] Handle multi-value and dictionary arguments. --- .../Diagnostics.cs | 20 +++ .../ParserGenerator.cs | 149 +++++++++++++++++- .../ParserIncrementalGenerator.cs | 3 +- .../Properties/Resources.Designer.cs | 36 +++++ .../Properties/Resources.resx | 12 ++ .../SymbolExtensions.cs | 23 +++ .../Support/GeneratedArgument.cs | 49 ++++-- .../{ => Support}/ReflectionArgument.cs | 18 +-- 8 files changed, 283 insertions(+), 27 deletions(-) rename src/Ookii.CommandLine/{ => Support}/ReflectionArgument.cs (93%) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 153355db..525f3c55 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -40,6 +40,26 @@ public static Diagnostic ArgumentsClassIsGeneric(INamedTypeSymbol symbol) => Dia isEnabledByDefault: true), symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic InvalidArrayRank(IPropertySymbol property) => Diagnostic.Create( + new DiagnosticDescriptor( + "CL1004", + new LocalizableResourceString(nameof(Resources.InvalidArrayRankTitle), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.InvalidArrayRankMessageFormat), Resources.ResourceManager, typeof(Resources)), + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true), + property.Locations.FirstOrDefault(), property.Type.ToDisplayString(), property.Name); + + public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => Diagnostic.Create( + new DiagnosticDescriptor( + "CL1005", + new LocalizableResourceString(nameof(Resources.PropertyIsReadOnlyTitle), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.PropertyIsReadOnlyMessageFormat), Resources.ResourceManager, typeof(Resources)), + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true), + property.Locations.FirstOrDefault(), property.Type.ToDisplayString(), property.Name); + public static Diagnostic UnknownAttribute(AttributeData attribute) => Diagnostic.Create( new DiagnosticDescriptor( "CLW1001", diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index da04a033..6e2796d1 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -1,26 +1,31 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; using System; using System.Diagnostics; +using System.Globalization; +using System.Reflection; using System.Text; namespace Ookii.CommandLine.Generator; internal class ParserGenerator { + private readonly Compilation _compilation; private readonly SourceProductionContext _context; private readonly INamedTypeSymbol _argumentsClass; private readonly SourceBuilder _builder; - public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumentsClass) + public ParserGenerator(Compilation compilation, SourceProductionContext context, INamedTypeSymbol argumentsClass) { + _compilation = compilation; _context = context; _argumentsClass = argumentsClass; _builder = new(argumentsClass.ContainingNamespace); } - public static string? Generate(SourceProductionContext context, INamedTypeSymbol _argumentsClass) + public static string? Generate(Compilation compilation, SourceProductionContext context, INamedTypeSymbol argumentsClass) { - var generator = new ParserGenerator(context, _argumentsClass); + var generator = new ParserGenerator(compilation, context, argumentsClass); return generator.Generate(); } @@ -159,7 +164,8 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) throw new NotImplementedException(); } - var argumentType = (INamedTypeSymbol)property!.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + var originalArgumentType = (INamedTypeSymbol)property!.Type; + var argumentType = (INamedTypeSymbol)originalArgumentType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); var nullableArgumentType = argumentType.WithNullableAnnotation(NullableAnnotation.Annotated); string extra = string.Empty; if (!argumentType.IsReferenceType && !argumentType.IsNullableValueType()) @@ -167,17 +173,81 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) extra = "!"; } - // TODO: key/value converters - // TODO: AllowsNull + INamedTypeSymbol elementTypeWithNullable = argumentType; + INamedTypeSymbol? keyType = null; + INamedTypeSymbol? valueType = null; + var allowsNull = originalArgumentType.AllowsNull(); + var kind = "Ookii.CommandLine.ArgumentKind.SingleValue"; + if (property != null) + { + var multiValueType = DetermineMultiValueType(property, argumentType); + if (multiValueType is not var (collectionType, dictionaryType, multiValueElementType)) + { + return; + } + + if (dictionaryType != null) + { + Debug.Assert(multiValueElementType != null); + kind = "Ookii.CommandLine.ArgumentKind.Dictionary"; + elementTypeWithNullable = multiValueElementType!; + keyType = (INamedTypeSymbol)elementTypeWithNullable.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.NotAnnotated); + valueType = ((INamedTypeSymbol)elementTypeWithNullable.TypeArguments[1]); + allowsNull = valueType.AllowsNull(); + valueType = (INamedTypeSymbol)valueType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + // TODO: Converter + //if (converterType == null) + //{ + // converterType = typeof(KeyValuePairConverter<,>).MakeGenericType(genericArguments); + // var keyConverterType = keyArgumentConverterAttribute?.GetConverterType(); + // var valueConverterType = valueArgumentConverterAttribute?.GetConverterType(); + // info.Converter = (ArgumentConverter)Activator.CreateInstance(converterType, info.Parser.StringProvider, + // info.ArgumentName, info.AllowNull, keyConverterType, valueConverterType, info.KeyValueSeparator)!; + //} + } + else if (collectionType != null) + { + Debug.Assert(multiValueElementType != null); + kind = "Ookii.CommandLine.ArgumentKind.MultiValue"; + elementTypeWithNullable = multiValueElementType!; + allowsNull = elementTypeWithNullable.AllowsNull(); + } + } + else + { + kind = "Ookii.CommandLine.ArgumentKind.Method"; + } + + var elementType = elementTypeWithNullable; + if (elementType.IsNullableValueType()) + { + elementType = (INamedTypeSymbol)elementType.TypeArguments[0]; + } + + // TODO: Converters // 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.AppendLine("parser"); _builder.AppendLine($", argumentType: typeof({argumentType.ToDisplayString()})"); + _builder.AppendLine($", elementTypeWithNullable: typeof({elementTypeWithNullable.ToDisplayString()})"); + _builder.AppendLine($", elementType: typeof({elementType.ToDisplayString()})"); _builder.AppendLine($", memberName: \"{member.Name}\""); + _builder.AppendLine($", kind: {kind}"); _builder.AppendLine($", attribute: {commandLineArgumentAttribute.CreateInstantiation()}"); _builder.AppendLine(", converter: Ookii.CommandLine.Conversion.StringConverter.Instance"); + _builder.AppendLine($", allowsNull: {(allowsNull ? "true" : "false")}"); + if (keyType != null) + { + _builder.AppendLine($", keyType: typeof({keyType.ToDisplayString()})"); + } + + if (valueType != null) + { + _builder.AppendLine($", valueType: typeof({valueType.ToDisplayString()})"); + } + if (multiValueSeparator != null) { _builder.AppendLine($", multiValueSeparatorAttribute: {multiValueSeparator.CreateInstantiation()}"); @@ -213,7 +283,16 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) _builder.AppendLine($", validationAttributes: new Ookii.CommandLine.Validation.ArgumentValidationAttribute[] {{ {string.Join(", ", validators.Select(a => a.CreateInstantiation()))} }}"); } - _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.Name})target).{member.Name} = ({nullableArgumentType.ToDisplayString()})value{extra}"); + if (property?.SetMethod?.DeclaredAccessibility == Accessibility.Public) + { + _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.Name})target).{member.Name} = ({nullableArgumentType.ToDisplayString()})value{extra}"); + } + + if (property != null) + { + _builder.AppendLine($", getProperty: (target) => (({_argumentsClass.Name})target).{member.Name}"); + } + _builder.DecreaseIndent(); _builder.AppendLine($");"); } @@ -242,4 +321,60 @@ private static bool CheckAttribute(AttributeData data, string name, ref List it doesn't matter if the property is + // read-only or not. + if (argumentType.IsGenericType && argumentType.ConstructedFrom.ToDisplayString() == "System.Collections.Generic.Dictionary") + { + var keyValuePair = _compilation.GetTypeByMetadataName(typeof(KeyValuePair<,>).FullName)!; + var elementType = keyValuePair.Construct(argumentType.TypeArguments, argumentType.TypeArgumentNullableAnnotations); + return (null, argumentType, 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 = (INamedTypeSymbol)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("System.Collections.Generic.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("System.Collection.Generic.ICollection"); + if (collectionType != null) + { + var elementType = (INamedTypeSymbol)collectionType.TypeArguments[0]; + return (collectionType, null, elementType); + } + + // This is a read-only property with an unsupported type. + _context.ReportDiagnostic(Diagnostics.PropertyIsReadOnly(property)); + return null; + } } diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index 57703841..3cf19bcd 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -35,7 +35,6 @@ private static void Execute(Compilation compilation, ImmutableArray + /// Looks up a localized string similar to The multi-value argument defined by {0}.{1} 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 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 property {0}.{1} must have a public set accessor.. + /// + internal static string PropertyIsReadOnlyMessageFormat { + get { + return ResourceManager.GetString("PropertyIsReadOnlyMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An 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 attribute {0} is unknown and will be ignored by the GeneratedParserAttribute.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 0a38a606..2caea136 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -135,6 +135,18 @@ The arguments type must be a reference type. + + The multi-value argument defined by {0}.{1} must have an array rank of one. + + + A multi-value argument defined by an array properties must have an array rank of one. + + + The property {0}.{1} must have a public set accessor. + + + An argument property must have a public set accessor. + The attribute {0} is unknown and will be ignored by the GeneratedParserAttribute. diff --git a/src/Ookii.CommandLine.Generator/SymbolExtensions.cs b/src/Ookii.CommandLine.Generator/SymbolExtensions.cs index 867524ea..2a922e98 100644 --- a/src/Ookii.CommandLine.Generator/SymbolExtensions.cs +++ b/src/Ookii.CommandLine.Generator/SymbolExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using System; using System.Text; namespace Ookii.CommandLine.Generator; @@ -25,6 +26,28 @@ public static bool DerivesFrom(this INamedTypeSymbol symbol, string baseClassNam public static bool IsNullableValueType(this INamedTypeSymbol symbol) => !symbol.IsReferenceType && symbol.IsGenericType && symbol.ConstructedFrom.ToDisplayString() == "System.Nullable"; + public static bool AllowsNull(this INamedTypeSymbol type) + => type.IsNullableValueType() || (type.IsReferenceType && type.NullableAnnotation != NullableAnnotation.NotAnnotated); + + public static INamedTypeSymbol? FindGenericInterface(this INamedTypeSymbol symbol, string interfaceName) + { + foreach (var iface in symbol.AllInterfaces) + { + var realIface = iface; + if (iface.IsGenericType) + { + realIface = iface.ConstructedFrom; + } + + if (realIface.ToDisplayString() == interfaceName) + { + return iface; + } + } + + return null; + } + public static string CreateInstantiation(this AttributeData attribute) { var ctorArgs = attribute.ConstructorArguments.Select(c => c.ToCSharpString()); diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 728088a9..4f4658dd 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -16,19 +16,28 @@ namespace Ookii.CommandLine.Support; public class GeneratedArgument : CommandLineArgument { private readonly Action? _setProperty; + private readonly Func? _getProperty; - private GeneratedArgument(ArgumentInfo info, Action? setProperty) : base(info) + private GeneratedArgument(ArgumentInfo info, Action? setProperty, Func? getProperty) : base(info) { _setProperty = setProperty; + _getProperty = getProperty; } /// + /// /// /// /// - /// /// /// + /// + /// + /// + /// + /// + /// + /// /// /// /// @@ -36,15 +45,20 @@ private GeneratedArgument(ArgumentInfo info, Action? setPropert /// /// /// - /// /// + /// /// public static GeneratedArgument Create(CommandLineParser parser, Type argumentType, + Type elementTypeWithNullable, + Type elementType, string memberName, CommandLineArgumentAttribute attribute, + ArgumentKind kind, ArgumentConverter converter, - bool allowsNull = false, + bool allowsNull, + Type? keyType = null, + Type? valueType = null, MultiValueSeparatorAttribute? multiValueSeparatorAttribute = null, DescriptionAttribute? descriptionAttribute = null, bool allowDuplicateDictionaryKeys = false, @@ -52,27 +66,44 @@ public static GeneratedArgument Create(CommandLineParser parser, IEnumerable? aliasAttributes = null, IEnumerable? shortAliasAttributes = null, IEnumerable? validationAttributes = null, - Action? setProperty = null) + Action? setProperty = null, + Func? getProperty = null) { var info = CreateArgumentInfo(parser, argumentType, allowsNull, memberName, attribute, multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); // TODO: Set properly for multi-value and Nullable. - info.ElementType = argumentType; + info.ElementType = elementType; + info.ElementTypeWithNullable = elementTypeWithNullable; info.Converter = converter; + info.Kind = kind; + if (info.Kind == ArgumentKind.Dictionary) + { + info.KeyValueSeparator ??= KeyValuePairConverter.DefaultSeparator; + info.KeyType = keyType; + info.ValueType = valueType; + } - return new GeneratedArgument(info, setProperty); + return new GeneratedArgument(info, setProperty, getProperty); } /// - protected override bool CanSetProperty => true; + protected override bool CanSetProperty => _setProperty != null; /// protected override bool CallMethod(object? value) => throw new NotImplementedException(); /// - protected override object? GetProperty(object target) => throw new NotImplementedException(); + protected override object? GetProperty(object target) + { + if (_getProperty == null) + { + throw new InvalidOperationException(); + } + + return _getProperty(target); + } /// protected override void SetProperty(object target, object? value) diff --git a/src/Ookii.CommandLine/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs similarity index 93% rename from src/Ookii.CommandLine/ReflectionArgument.cs rename to src/Ookii.CommandLine/Support/ReflectionArgument.cs index 13407902..1f769c0d 100644 --- a/src/Ookii.CommandLine/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -9,7 +9,7 @@ using Ookii.CommandLine.Validation; using System.Threading; -namespace Ookii.CommandLine; +namespace Ookii.CommandLine.Support; internal class ReflectionArgument : CommandLineArgument { @@ -151,7 +151,7 @@ private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo me if (member is PropertyInfo property) { - var (collectionType, dictionaryType, elementType) = + var (collectionType, dictionaryType, elementType) = DetermineMultiValueType(info.ArgumentName, info.ArgumentType, property); if (dictionaryType != null) @@ -229,14 +229,14 @@ private static (Type?, Type?, Type?) DetermineMultiValueType(string argumentName return (null, null, null); } - var dictionaryType = TypeHelper.FindGenericInterface(argumentType, typeof(IDictionary<,>)); + var dictionaryType = argumentType.FindGenericInterface(typeof(IDictionary<,>)); if (dictionaryType != null) { var elementType = typeof(KeyValuePair<,>).MakeGenericType(dictionaryType.GetGenericArguments()); return (null, dictionaryType, elementType); } - var collectionType = TypeHelper.FindGenericInterface(argumentType, typeof(ICollection<>)); + var collectionType = argumentType.FindGenericInterface(typeof(ICollection<>)); if (collectionType != null) { var elementType = collectionType.GetGenericArguments()[0]; @@ -291,8 +291,8 @@ private static bool DetermineCollectionElementTypeAllowsNull(Type type, Property // 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<>)))) + if (actualType != null && (actualType.IsArray || actualType.IsGenericType && + actualType.GetGenericTypeDefinition() == typeof(ICollection<>))) { var context = new NullabilityInfoContext(); var info = context.Create(property); @@ -323,7 +323,7 @@ private static bool DetermineAllowsNull(PropertyInfo property) var info = context.Create(property); return info.WriteState != NullabilityState.NotNull; #else - return true; + return true; #endif } @@ -340,7 +340,7 @@ private static bool DetermineAllowsNull(ParameterInfo parameter) var info = context.Create(parameter); return info.WriteState != NullabilityState.NotNull; #else - return true; + return true; #endif } @@ -358,7 +358,7 @@ private static (MethodArgumentInfo, Type, bool)? DetermineMethodArgumentInfo(Met { var parameters = method.GetParameters(); if (!method.IsStatic || - (method.ReturnType != typeof(bool) && method.ReturnType != typeof(void)) || + method.ReturnType != typeof(bool) && method.ReturnType != typeof(void) || parameters.Length > 2) { return null; From 1fdbd019984edaf890cdad04eca70825637f7596 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 10 Apr 2023 15:23:11 -0700 Subject: [PATCH 019/234] Select the correct converter if an existing one applies. --- .../AttributeNames.cs | 2 + .../Diagnostics.cs | 14 ++- .../ParserGenerator.cs | 108 +++++++++++++++--- .../Properties/Resources.Designer.cs | 18 +++ .../Properties/Resources.resx | 6 + .../SymbolExtensions.cs | 18 +++ .../Conversion/NullableConverter.cs | 19 ++- src/Samples/TrimTest/Program.cs | 2 +- 8 files changed, 168 insertions(+), 19 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/AttributeNames.cs b/src/Ookii.CommandLine.Generator/AttributeNames.cs index da7cb0d5..50f1b7c1 100644 --- a/src/Ookii.CommandLine.Generator/AttributeNames.cs +++ b/src/Ookii.CommandLine.Generator/AttributeNames.cs @@ -20,6 +20,8 @@ internal static class AttributeNames public const string ShortAlias = NamespacePrefix + "ShortAliasAttribute"; public const string ArgumentValidation = NamespacePrefix + "Validation.ArgumentValidationAttribute"; public const string ArgumentConverter = NamespacePrefix + "Conversion.ArgumentConverterAttribute"; + public const string KeyConverter = NamespacePrefix + "Conversion.KeyConverterAttribute"; + public const string ValueConverter = NamespacePrefix + "Conversion.ValueConverterAttribute"; public const string Description = "System.ComponentModel.DescriptionAttribute"; } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 525f3c55..c54260a3 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -48,7 +48,7 @@ public static Diagnostic InvalidArrayRank(IPropertySymbol property) => Diagnosti Category, DiagnosticSeverity.Error, isEnabledByDefault: true), - property.Locations.FirstOrDefault(), property.Type.ToDisplayString(), property.Name); + property.Locations.FirstOrDefault(), property.ContainingType?.ToDisplayString(), property.Name); public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => Diagnostic.Create( new DiagnosticDescriptor( @@ -58,7 +58,17 @@ public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => Diagnos Category, DiagnosticSeverity.Error, isEnabledByDefault: true), - property.Locations.FirstOrDefault(), property.Type.ToDisplayString(), property.Name); + property.Locations.FirstOrDefault(), property.ContainingType?.ToDisplayString(), property.Name); + + public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => Diagnostic.Create( + new DiagnosticDescriptor( + "CL1006", + new LocalizableResourceString(nameof(Resources.NoConverterTitle), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.NoConverterMessageFormat), Resources.ResourceManager, typeof(Resources)), + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true), + member.Locations.FirstOrDefault(), elementType.ToDisplayString(), member.ContainingType?.ToDisplayString(), member.Name); public static Diagnostic UnknownAttribute(AttributeData attribute) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 6e2796d1..9d4841aa 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -1,6 +1,8 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Operations; using System; +using System.Data; using System.Diagnostics; using System.Globalization; using System.Reflection; @@ -129,7 +131,9 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) AttributeData? description = null; AttributeData? allowDuplicateDictionaryKeys = null; AttributeData? keyValueSeparator = null; - AttributeData? converter = null; + AttributeData? converterAttribute = null; + AttributeData? keyConverterAttribute = null; + AttributeData? valueConverterAttribute = null; List? aliases = null; List? shortAliases = null; List? validators = null; @@ -140,7 +144,9 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) CheckAttribute(attribute, AttributeNames.Description, ref description) || CheckAttribute(attribute, AttributeNames.AllowDuplicateDictionaryKeys, ref allowDuplicateDictionaryKeys) || CheckAttribute(attribute, AttributeNames.KeyValueSeparator, ref keyValueSeparator) || - CheckAttribute(attribute, AttributeNames.ArgumentConverter, ref converter) || + CheckAttribute(attribute, AttributeNames.ArgumentConverter, ref converterAttribute) || + CheckAttribute(attribute, AttributeNames.KeyConverter, ref keyConverterAttribute) || + CheckAttribute(attribute, AttributeNames.ValueConverter, ref valueConverterAttribute) || CheckAttribute(attribute, AttributeNames.Alias, ref aliases) || CheckAttribute(attribute, AttributeNames.ShortAlias, ref shortAliases) || CheckAttribute(attribute, AttributeNames.ArgumentValidation, ref validators)) @@ -178,6 +184,7 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) INamedTypeSymbol? valueType = null; var allowsNull = originalArgumentType.AllowsNull(); var kind = "Ookii.CommandLine.ArgumentKind.SingleValue"; + string? converter = null; if (property != null) { var multiValueType = DetermineMultiValueType(property, argumentType); @@ -196,14 +203,27 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) allowsNull = valueType.AllowsNull(); valueType = (INamedTypeSymbol)valueType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); // TODO: Converter - //if (converterType == null) - //{ - // converterType = typeof(KeyValuePairConverter<,>).MakeGenericType(genericArguments); - // var keyConverterType = keyArgumentConverterAttribute?.GetConverterType(); - // var valueConverterType = valueArgumentConverterAttribute?.GetConverterType(); - // info.Converter = (ArgumentConverter)Activator.CreateInstance(converterType, info.Parser.StringProvider, - // info.ArgumentName, info.AllowNull, keyConverterType, valueConverterType, info.KeyValueSeparator)!; - //} + if (converterAttribute == null) + { + var keyConverter = DetermineConverter(keyType.GetUnderlyingType(), keyConverterAttribute, keyType.IsNullableValueType()); + if (keyConverter == null) + { + _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); + return; + } + + var valueConverter = DetermineConverter(valueType.GetUnderlyingType(), valueConverterAttribute, valueType.IsNullableValueType()); + if (keyConverter == null) + { + _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); + return; + } + + // TODO: Need to create this in argument, change approach: instead of passing KeyType/ValueType, + // pass KeyConverter/ValueConverter in info so code can be used for both reflection and generated. + //info.Converter = (ArgumentConverter)Activator.CreateInstance(converterType, info.Parser.StringProvider, + // info.ArgumentName, info.AllowNull, keyConverterType, valueConverterType, info.KeyValueSeparator)!; + } } else if (collectionType != null) { @@ -219,13 +239,16 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) } var elementType = elementTypeWithNullable; - if (elementType.IsNullableValueType()) + elementType = elementTypeWithNullable.GetUnderlyingType(); + converter ??= DetermineConverter(elementType, converterAttribute, elementTypeWithNullable.IsNullableValueType()); + if (converter == null) { - elementType = (INamedTypeSymbol)elementType.TypeArguments[0]; + _context.ReportDiagnostic(Diagnostics.NoConverter(member, elementType)); + return; } - // TODO: Converters - + // TODO: Default value description. Can make DetermineValueDescription abstract and move + // to ReflectionArgument when done. // 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(); @@ -236,7 +259,7 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) _builder.AppendLine($", memberName: \"{member.Name}\""); _builder.AppendLine($", kind: {kind}"); _builder.AppendLine($", attribute: {commandLineArgumentAttribute.CreateInstantiation()}"); - _builder.AppendLine(", converter: Ookii.CommandLine.Conversion.StringConverter.Instance"); + _builder.AppendLine($", converter: {converter}"); _builder.AppendLine($", allowsNull: {(allowsNull ? "true" : "false")}"); if (keyType != null) { @@ -377,4 +400,59 @@ private static bool CheckAttribute(AttributeData data, string name, ref List")) + { + return $"new Ookii.CommandLine.Conversion.SpanParsableConverter<{elementType.ToDisplayString()}>()"; + } + + if (elementType.ImplementsInterface($"System.IParsable<{elementType.ToDisplayString()}>")) + { + return $"new Ookii.CommandLine.Conversion.ParsableConverter<{elementType.ToDisplayString()}>()"; + } + + // TODO: Generate a converter. + return null; + } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 3cdc11b3..373262e7 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -132,6 +132,24 @@ internal static string InvalidArrayRankTitle { } } + /// + /// Looks up a localized string similar to No argument converter exists for type {0} used by the argument defined by {1}.{2}, 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 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 property {0}.{1} must have a public set accessor.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 2caea136..0b094837 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -141,6 +141,12 @@ A multi-value argument defined by an array properties must have an array rank of one. + + No argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter. + + + No argument converter exists for the argument's type. + The property {0}.{1} must have a public set accessor. diff --git a/src/Ookii.CommandLine.Generator/SymbolExtensions.cs b/src/Ookii.CommandLine.Generator/SymbolExtensions.cs index 2a922e98..3db13d19 100644 --- a/src/Ookii.CommandLine.Generator/SymbolExtensions.cs +++ b/src/Ookii.CommandLine.Generator/SymbolExtensions.cs @@ -29,6 +29,11 @@ public static bool IsNullableValueType(this INamedTypeSymbol symbol) public static bool AllowsNull(this INamedTypeSymbol type) => type.IsNullableValueType() || (type.IsReferenceType && type.NullableAnnotation != NullableAnnotation.NotAnnotated); + public static INamedTypeSymbol GetUnderlyingType(this INamedTypeSymbol type) + => type.IsNullableValueType() ? (INamedTypeSymbol)type.TypeArguments[0] : type; + + public static bool IsEnum(this ITypeSymbol type) => type.BaseType?.ToDisplayString() == "System.Enum"; + public static INamedTypeSymbol? FindGenericInterface(this INamedTypeSymbol symbol, string interfaceName) { foreach (var iface in symbol.AllInterfaces) @@ -48,6 +53,19 @@ public static bool AllowsNull(this INamedTypeSymbol type) return null; } + public static bool ImplementsInterface(this INamedTypeSymbol symbol, string interfaceName) + { + foreach (var iface in symbol.AllInterfaces) + { + if (iface.ToDisplayString() == interfaceName) + { + return true; + } + } + + return false; + } + public static string CreateInstantiation(this AttributeData attribute) { var ctorArgs = attribute.ConstructorArguments.Select(c => c.ToCSharpString()); diff --git a/src/Ookii.CommandLine/Conversion/NullableConverter.cs b/src/Ookii.CommandLine/Conversion/NullableConverter.cs index 7bfbd59e..6dd4925d 100644 --- a/src/Ookii.CommandLine/Conversion/NullableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/NullableConverter.cs @@ -3,15 +3,31 @@ namespace Ookii.CommandLine.Conversion; -internal class NullableConverter : ArgumentConverter +/// +/// 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 standard . +/// +/// +/// +public class NullableConverter : ArgumentConverter { private readonly ArgumentConverter _baseConverter; + /// + /// Initializes a new instance of the class. + /// + /// The converter to use for the type T. public NullableConverter(ArgumentConverter baseConverter) { _baseConverter = baseConverter; } + /// public override object? Convert(string value, CultureInfo culture) { if (value.Length == 0) @@ -22,6 +38,7 @@ public NullableConverter(ArgumentConverter baseConverter) return _baseConverter.Convert(value, culture); } + /// public override object? Convert(ReadOnlySpan value, CultureInfo culture) { if (value.Length == 0) diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index a9d59e31..3205629a 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -28,7 +28,7 @@ partial class Arguments [CommandLineArgument(ValueDescription = "Stuff")] [KeyValueSeparator("==")] [MultiValueSeparator] - public Dictionary? Test2 { get; set; } = default!; + public bool Test2 { get; set; } = default!; [CommandLineArgument] public int Test3 { get; set; } From 0afd154af2bc0f0131d821bf4e2adddc92764744 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 10 Apr 2023 15:50:35 -0700 Subject: [PATCH 020/234] Add argument parameter to ArgumentConverter. --- .../CommandLineParserNullableTest.cs | 4 +- .../KeyValuePairConverterTest.cs | 15 ++++- src/Ookii.CommandLine/CommandLineArgument.cs | 8 +-- .../Conversion/ArgumentConverter.cs | 34 +++++++--- .../Conversion/BooleanConverter.cs | 4 +- .../Conversion/ConstructorConverter.cs | 7 +- .../Conversion/EnumConverter.cs | 4 +- .../Conversion/KeyValuePairConverter.cs | 64 +++++++++---------- .../Conversion/NullableConverter.cs | 8 +-- .../Conversion/ParsableConverter.cs | 2 +- .../Conversion/ParseConverter.cs | 7 +- .../Conversion/SpanParsableConverter.cs | 5 +- .../Conversion/StringConverter.cs | 2 +- .../Support/ReflectionArgument.cs | 8 +-- .../Validation/ValidateNotNullAttribute.cs | 3 +- src/Samples/Subcommand/EncodingConverter.cs | 6 +- 16 files changed, 105 insertions(+), 76 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs index d0660457..9b503848 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -19,7 +19,7 @@ public class CommandLineParserNullableTest class NullReturningStringConverter : ArgumentConverter { - public override object? Convert(string value, CultureInfo culture) + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) { if (value == "(null)") { @@ -34,7 +34,7 @@ class NullReturningStringConverter : ArgumentConverter class NullReturningIntConverter : ArgumentConverter { - public override object? Convert(string value, CultureInfo culture) + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) { if (value == "(null)") { diff --git a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs index 06be1716..2ec04866 100644 --- a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs +++ b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs @@ -8,19 +8,28 @@ namespace Ookii.CommandLine.Tests [TestClass] public class KeyValuePairConverterTest { + // Needed because SpanParsableConverter only exists on .Net 7. + private class IntConverter : ArgumentConverter + { + public override object Convert(string value, CultureInfo culture, CommandLineArgument argument) + => int.Parse(value, culture); + } + [TestMethod] public void TestConvertFrom() { + var parser = new CommandLineParser(); var converter = new KeyValuePairConverter(); - var converted = converter.Convert("foo=5", CultureInfo.InvariantCulture); + var converted = converter.Convert("foo=5", CultureInfo.InvariantCulture, parser.GetArgument("Argument1")); Assert.AreEqual(KeyValuePair.Create("foo", 5), converted); } [TestMethod] public void TestCustomSeparator() { - var converter = new KeyValuePairConverter(new LocalizedStringProvider(), "Test", false, null, null, ":"); - var pair = converter.Convert("foo:5", CultureInfo.InvariantCulture); + 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/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 0f571ed0..4cd36a9b 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -929,7 +929,7 @@ public bool AllowsDuplicateDictionaryKeys /// /// /// This property indicates what happens when the used for this argument returns - /// from its + /// from its /// method. /// /// @@ -1096,7 +1096,7 @@ public bool AllowsDuplicateDictionaryKeys return null; } - return _converter.Convert(stringValue, CultureInfo.InvariantCulture); + return _converter.Convert(stringValue, CultureInfo.InvariantCulture, this); } /// @@ -1268,8 +1268,8 @@ private static string GetFriendlyTypeName(Type type) try { var converted = stringValue == null - ? _converter.Convert(spanValue, culture) - : _converter.Convert(stringValue, culture); + ? _converter.Convert(spanValue, culture, this) + : _converter.Convert(stringValue, culture, this); if (converted == null && (!_allowNull || IsDictionary)) { diff --git a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs index 021d7e24..4d7a533c 100644 --- a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs @@ -9,9 +9,10 @@ namespace Ookii.CommandLine.Conversion; /// /// /// 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. +/// 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 @@ -21,30 +22,47 @@ public abstract class ArgumentConverter /// /// 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 value was not in a correct format for the target type. /// - public abstract object? Convert(string value, CultureInfo culture); + /// + /// The value was not in a correct format for the target type. Unlike , + /// 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 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 - /// . Implement this method if it's possible to - /// + /// . Override this method if + /// a direct conversion from a is possible for the target + /// type. /// /// /// /// The value was not in a correct format for the target type. /// - public virtual object? Convert(ReadOnlySpan value, CultureInfo culture) + /// + /// The value was not in a correct format for the target type. Unlike , + /// 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); + return Convert(value.ToString(), culture, argument); } } diff --git a/src/Ookii.CommandLine/Conversion/BooleanConverter.cs b/src/Ookii.CommandLine/Conversion/BooleanConverter.cs index 1b1a2124..5647b3cd 100644 --- a/src/Ookii.CommandLine/Conversion/BooleanConverter.cs +++ b/src/Ookii.CommandLine/Conversion/BooleanConverter.cs @@ -15,10 +15,10 @@ public class BooleanConverter : ArgumentConverter public static readonly BooleanConverter Instance = new(); /// - public override object? Convert(string value, CultureInfo culture) => bool.Parse(value); + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) => bool.Parse(value); #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER /// - public override object? Convert(ReadOnlySpan value, CultureInfo culture) => bool.Parse(value); + 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 index 87503379..482cd9cd 100644 --- a/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs @@ -12,15 +12,16 @@ public ConstructorConverter(Type type) _type = type; } - public override object? Convert(string value, CultureInfo culture) + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) { try { return _type.CreateInstance(value); } - catch (CommandLineArgumentException) + catch (CommandLineArgumentException ex) { - throw; + // Patch the exception with the argument name. + throw new CommandLineArgumentException(ex.Message, argument.ArgumentName, ex.Category, ex.InnerException); } catch (FormatException) { diff --git a/src/Ookii.CommandLine/Conversion/EnumConverter.cs b/src/Ookii.CommandLine/Conversion/EnumConverter.cs index abf0f289..b0032b78 100644 --- a/src/Ookii.CommandLine/Conversion/EnumConverter.cs +++ b/src/Ookii.CommandLine/Conversion/EnumConverter.cs @@ -12,7 +12,7 @@ public EnumConverter(Type enumType) _enumType = enumType; } - public override object? Convert(string value, CultureInfo culture) + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) { try { @@ -29,7 +29,7 @@ public EnumConverter(Type enumType) } #if NET6_0_OR_GREATER - public override object? Convert(ReadOnlySpan value, CultureInfo culture) + public override object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) { try { diff --git a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs index 5f1a3567..90356550 100644 --- a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs +++ b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs @@ -31,39 +31,36 @@ public class KeyValuePairConverter : ArgumentConverter { private readonly ArgumentConverter _keyConverter; private readonly ArgumentConverter _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. - /// - /// - /// 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) + /// + /// Provides the used to convert the key/value pair's keys. + /// + /// + /// Provides the used to convert the key/value pair's values. + /// + /// + /// Provides 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) { - _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); + _keyConverter = keyConverter ?? throw new ArgumentNullException(nameof(keyConverter)); + _valueConverter = valueConverter ?? throw new ArgumentNullException(nameof(valueConverter)); _separator = separator ?? KeyValuePairConverter.DefaultSeparator; if (_separator.Length == 0) { @@ -75,28 +72,29 @@ public KeyValuePairConverter(LocalizedStringProvider stringProvider, string argu /// Initializes a new instance of the class. /// public KeyValuePairConverter() - : this(new LocalizedStringProvider(), string.Empty, true, null, null, null) + : this(typeof(TKey).GetStringConverter(null), typeof(TValue).GetStringConverter(null), null, true) { } /// - public override object? Convert(string value, CultureInfo culture) - => Convert((value ?? throw new ArgumentNullException(nameof(value))).AsSpan(), culture); + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) + => Convert((value ?? throw new ArgumentNullException(nameof(value))).AsSpan(), culture, argument); /// - public override object? Convert(ReadOnlySpan value, CultureInfo culture) + public override object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) { var (key, valueForKey) = value.SplitOnce(_separator.AsSpan(), out bool hasSeparator); if (!hasSeparator) { - throw new FormatException(_stringProvider.MissingKeyValuePairSeparator(_separator)); + throw new FormatException(argument.Parser.StringProvider.MissingKeyValuePairSeparator(_separator)); } - object? convertedKey = _keyConverter.Convert(key, culture); - object? convertedValue = _valueConverter.Convert(valueForKey, culture); + var convertedKey = _keyConverter.Convert(key, culture, argument); + var convertedValue = _valueConverter.Convert(valueForKey, culture, argument); if (convertedKey == null || !_allowNullValues && convertedValue == null) { - throw _stringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, _argumentName); + throw argument.Parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, + argument.ArgumentName); } return new KeyValuePair((TKey)convertedKey, (TValue?)convertedValue); diff --git a/src/Ookii.CommandLine/Conversion/NullableConverter.cs b/src/Ookii.CommandLine/Conversion/NullableConverter.cs index 6dd4925d..586df100 100644 --- a/src/Ookii.CommandLine/Conversion/NullableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/NullableConverter.cs @@ -28,24 +28,24 @@ public NullableConverter(ArgumentConverter baseConverter) } /// - public override object? Convert(string value, CultureInfo culture) + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) { if (value.Length == 0) { return null; } - return _baseConverter.Convert(value, culture); + return _baseConverter.Convert(value, culture, argument); } /// - public override object? Convert(ReadOnlySpan value, CultureInfo culture) + public override object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) { if (value.Length == 0) { return null; } - return _baseConverter.Convert(value, culture); + return _baseConverter.Convert(value, culture, argument); } } diff --git a/src/Ookii.CommandLine/Conversion/ParsableConverter.cs b/src/Ookii.CommandLine/Conversion/ParsableConverter.cs index e755bae4..ffc8e5ec 100644 --- a/src/Ookii.CommandLine/Conversion/ParsableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ParsableConverter.cs @@ -24,7 +24,7 @@ public class ParsableConverter : ArgumentConverter where T : IParsable { /// - public override object? Convert(string value, CultureInfo culture) => T.Parse(value, culture); + 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 index 348d3ae6..34a8edd1 100644 --- a/src/Ookii.CommandLine/Conversion/ParseConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ParseConverter.cs @@ -15,7 +15,7 @@ public ParseConverter(MethodInfo method, bool hasCulture) _hasCulture = hasCulture; } - public override object? Convert(string value, CultureInfo culture) + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) { var parameters = _hasCulture ? new object?[] { value, culture } @@ -25,9 +25,10 @@ public ParseConverter(MethodInfo method, bool hasCulture) { return _method.Invoke(null, parameters); } - catch (CommandLineArgumentException) + catch (CommandLineArgumentException ex) { - throw; + // Patch the exception with the argument name. + throw new CommandLineArgumentException(ex.Message, argument.ArgumentName, ex.Category, ex.InnerException); } catch (FormatException) { diff --git a/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs index 1bc6b0b3..995868bd 100644 --- a/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs @@ -23,10 +23,11 @@ public class SpanParsableConverter : ArgumentConverter where T : ISpanParsable { /// - public override object? Convert(string value, CultureInfo culture) => T.Parse(value, culture); + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) => T.Parse(value, culture); /// - public override object? Convert(ReadOnlySpan value, CultureInfo culture) => 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 index 1bd1de8c..b549638c 100644 --- a/src/Ookii.CommandLine/Conversion/StringConverter.cs +++ b/src/Ookii.CommandLine/Conversion/StringConverter.cs @@ -20,5 +20,5 @@ public class StringConverter : ArgumentConverter public static readonly StringConverter Instance = new(); /// - public override object? Convert(string value, CultureInfo culture) => value; + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) => value; } diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index 1f769c0d..4ae7cc25 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -167,10 +167,10 @@ private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo me if (converterType == null) { converterType = typeof(KeyValuePairConverter<,>).MakeGenericType(genericArguments); - var keyConverterType = keyArgumentConverterAttribute?.GetConverterType(); - var valueConverterType = valueArgumentConverterAttribute?.GetConverterType(); - info.Converter = (ArgumentConverter)Activator.CreateInstance(converterType, info.Parser.StringProvider, - info.ArgumentName, info.AllowNull, keyConverterType, valueConverterType, info.KeyValueSeparator)!; + var keyConverter = info.KeyType.GetStringConverter(keyArgumentConverterAttribute?.GetConverterType()); + var valueConverter = info.ValueType.GetStringConverter(valueArgumentConverterAttribute?.GetConverterType()); + info.Converter = (ArgumentConverter)Activator.CreateInstance(converterType, keyConverter, valueConverter, + info.KeyValueSeparator, info.AllowNull)!; } } else if (collectionType != null) diff --git a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs index 718d22fe..c3ac690f 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs @@ -10,7 +10,8 @@ namespace Ookii.CommandLine.Validation /// /// /// An argument's value can only be if its - /// returns from the + /// returns from the + /// /// method. For example, the can return . /// /// diff --git a/src/Samples/Subcommand/EncodingConverter.cs b/src/Samples/Subcommand/EncodingConverter.cs index e7b7f041..822eca4c 100644 --- a/src/Samples/Subcommand/EncodingConverter.cs +++ b/src/Samples/Subcommand/EncodingConverter.cs @@ -1,6 +1,6 @@ -using Ookii.CommandLine.Conversion; +using Ookii.CommandLine; +using Ookii.CommandLine.Conversion; using System; -using System.ComponentModel; using System.Globalization; using System.Text; @@ -10,7 +10,7 @@ namespace SubcommandSample; // Ookii.CommandLine. internal class EncodingConverter : ArgumentConverter { - public override object? Convert(string value, CultureInfo culture) + public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) { try { From 20669868f8453b8de49dcedcdf91a35d49b47be5 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 10 Apr 2023 16:35:37 -0700 Subject: [PATCH 021/234] Support key/value pair conversion. --- .../{SymbolExtensions.cs => Extensions.cs} | 7 +++- .../ParserGenerator.cs | 35 +++++++++++-------- src/Samples/TrimTest/Program.cs | 2 +- 3 files changed, 27 insertions(+), 17 deletions(-) rename src/Ookii.CommandLine.Generator/{SymbolExtensions.cs => Extensions.cs} (91%) diff --git a/src/Ookii.CommandLine.Generator/SymbolExtensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs similarity index 91% rename from src/Ookii.CommandLine.Generator/SymbolExtensions.cs rename to src/Ookii.CommandLine.Generator/Extensions.cs index 3db13d19..e0c5d079 100644 --- a/src/Ookii.CommandLine.Generator/SymbolExtensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -5,7 +5,7 @@ namespace Ookii.CommandLine.Generator; -internal static class SymbolExtensions +internal static class Extensions { public static bool DerivesFrom(this INamedTypeSymbol symbol, string baseClassName) { @@ -72,4 +72,9 @@ public static string CreateInstantiation(this AttributeData attribute) var namedArgs = attribute.NamedArguments.Select(n => $"{n.Key} = {n.Value.ToCSharpString()}"); return $"new {attribute.AttributeClass?.ToDisplayString()}({string.Join(", ", ctorArgs)}) {{ {string.Join(", ", namedArgs)} }}"; } + + public static string ToCSharpString(this bool value) + { + return value ? "true" : "false"; + } } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 9d4841aa..1e051122 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -119,7 +119,7 @@ private void GenerateProvider() private void GenerateArgument(ISymbol member) { - // Check if the member can be an argument. + // Check if the member can be an argument. TODO: warning if private. if (member.DeclaredAccessibility != Accessibility.Public || member.Kind is not (SymbolKind.Method or SymbolKind.Property)) { @@ -172,9 +172,9 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) var originalArgumentType = (INamedTypeSymbol)property!.Type; var argumentType = (INamedTypeSymbol)originalArgumentType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - var nullableArgumentType = argumentType.WithNullableAnnotation(NullableAnnotation.Annotated); string extra = string.Empty; - if (!argumentType.IsReferenceType && !argumentType.IsNullableValueType()) + var allowsNull = originalArgumentType.AllowsNull(); + if (!allowsNull) { extra = "!"; } @@ -182,7 +182,11 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) INamedTypeSymbol elementTypeWithNullable = argumentType; INamedTypeSymbol? keyType = null; INamedTypeSymbol? valueType = null; - var allowsNull = originalArgumentType.AllowsNull(); + if (keyValueSeparator != null) + { + _builder.AppendLine($"var keyValueSeparatorAttribute{member.Name} = {keyValueSeparator.CreateInstantiation()};"); + } + var kind = "Ookii.CommandLine.ArgumentKind.SingleValue"; string? converter = null; if (property != null) @@ -199,9 +203,9 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) kind = "Ookii.CommandLine.ArgumentKind.Dictionary"; elementTypeWithNullable = multiValueElementType!; keyType = (INamedTypeSymbol)elementTypeWithNullable.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.NotAnnotated); - valueType = ((INamedTypeSymbol)elementTypeWithNullable.TypeArguments[1]); - allowsNull = valueType.AllowsNull(); - valueType = (INamedTypeSymbol)valueType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + var rawValueType = ((INamedTypeSymbol)elementTypeWithNullable.TypeArguments[1]); + allowsNull = rawValueType.AllowsNull(); + valueType = (INamedTypeSymbol)rawValueType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); // TODO: Converter if (converterAttribute == null) { @@ -213,16 +217,17 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) } var valueConverter = DetermineConverter(valueType.GetUnderlyingType(), valueConverterAttribute, valueType.IsNullableValueType()); - if (keyConverter == null) + if (valueConverter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); return; } - // TODO: Need to create this in argument, change approach: instead of passing KeyType/ValueType, - // pass KeyConverter/ValueConverter in info so code can be used for both reflection and generated. - //info.Converter = (ArgumentConverter)Activator.CreateInstance(converterType, info.Parser.StringProvider, - // info.ArgumentName, info.AllowNull, keyConverterType, valueConverterType, info.KeyValueSeparator)!; + var separator = keyValueSeparator == null + ? "null" + : $"keyValueSeparatorAttribute{member.Name}.Separator"; + + converter = $"new Ookii.CommandLine.Conversion.KeyValuePairConverter<{keyType.ToDisplayString()}, {rawValueType.ToDisplayString()}>({keyConverter}, {valueConverter}, {separator}, {allowsNull.ToCSharpString()})"; } } else if (collectionType != null) @@ -260,7 +265,7 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) _builder.AppendLine($", kind: {kind}"); _builder.AppendLine($", attribute: {commandLineArgumentAttribute.CreateInstantiation()}"); _builder.AppendLine($", converter: {converter}"); - _builder.AppendLine($", allowsNull: {(allowsNull ? "true" : "false")}"); + _builder.AppendLine($", allowsNull: {(allowsNull.ToCSharpString())}"); if (keyType != null) { _builder.AppendLine($", keyType: typeof({keyType.ToDisplayString()})"); @@ -288,7 +293,7 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) if (keyValueSeparator != null) { - _builder.AppendLine($", keyValueSeparatorAttribute: {keyValueSeparator.CreateInstantiation()}"); + _builder.AppendLine($", keyValueSeparatorAttribute: keyValueSeparatorAttribute{member.Name}"); } if (aliases != null) @@ -308,7 +313,7 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) if (property?.SetMethod?.DeclaredAccessibility == Accessibility.Public) { - _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.Name})target).{member.Name} = ({nullableArgumentType.ToDisplayString()})value{extra}"); + _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.Name})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{extra}"); } if (property != null) diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 3205629a..d976e73a 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -28,7 +28,7 @@ partial class Arguments [CommandLineArgument(ValueDescription = "Stuff")] [KeyValueSeparator("==")] [MultiValueSeparator] - public bool Test2 { get; set; } = default!; + public Dictionary Test2 { get; set; } = default!; [CommandLineArgument] public int Test3 { get; set; } From 4809ae0e23bd4f5ab0472d68fc10ddf00f6a6206 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 10 Apr 2023 18:28:24 -0700 Subject: [PATCH 022/234] Support generated converter for types with constructors. --- .../ConverterGenerator.cs | 217 ++++++++++++++++++ src/Ookii.CommandLine.Generator/Extensions.cs | 41 +++- .../ParserGenerator.cs | 15 +- .../ParserIncrementalGenerator.cs | 10 +- .../SourceBuilder.cs | 9 +- src/Samples/TrimTest/Program.cs | 3 + 6 files changed, 281 insertions(+), 14 deletions(-) create mode 100644 src/Ookii.CommandLine.Generator/ConverterGenerator.cs diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs new file mode 100644 index 00000000..08aafd55 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -0,0 +1,217 @@ +using Microsoft.CodeAnalysis; +using System.Text; + +namespace Ookii.CommandLine.Generator; + +internal class ConverterGenerator +{ + #region Nested types + + private struct ConverterInfo + { + public string? Name { get; set; } + public bool ParseMethod { get; set; } + public bool HasCulture { get; set; } + public bool UseSpan { get; set; } + + public string ConstructorCall => $"new {GeneratedNamespace}.{Name}()"; + + public bool IsBetter(ConverterInfo other) + { + // Prefer Parse over constructor. + if (ParseMethod != other.ParseMethod) + { + return ParseMethod; + } + + // Prefer culture over no culture. + if (HasCulture != other.HasCulture) + { + return HasCulture; + } + + // Prefer span over string. + if (UseSpan != other.UseSpan) + { + return UseSpan; + } + + return false; + } + } + + #endregion + + // TODO: Customizable or random namespace? + private const string GeneratedNamespace = "Ookii.CommandLine.Conversion.Generated"; + private const string ConverterSuffix = "Converter"; + private readonly INamedTypeSymbol? _readOnlySpanType; + private readonly INamedTypeSymbol? _stringType; + private readonly INamedTypeSymbol? _cultureType; + + public ConverterGenerator(Compilation compilation) + { + _stringType = compilation.GetTypeByMetadataName("System.String"); + _cultureType = compilation.GetTypeByMetadataName("System.Globalization.CultureInfo"); + var charType = compilation.GetTypeByMetadataName("System.Char"); + if (charType != null) + { + _readOnlySpanType = compilation.GetTypeByMetadataName("System.ReadOnlySpan`1")?.Construct(charType); + } + } + + private readonly Dictionary _converters = new(SymbolEqualityComparer.Default); + + public string? GetConverter(INamedTypeSymbol type) + { + if (_converters.TryGetValue(type, out var converter)) + { + return converter.ConstructorCall; + } + + var optionalInfo = FindParseMethod(type) ?? FindConstructor(type); + if (optionalInfo is not ConverterInfo info) + { + return null; + } + + info.Name = GenerateName(type.ToDisplayString()); + _converters.Add(type, info); + return info.ConstructorCall; + } + + public string? Generate() + { + if (_converters.Count == 0) + { + return null; + } + + var builder = new SourceBuilder(GeneratedNamespace); + foreach (var converter in _converters) + { + CreateConverter(builder, converter.Key, converter.Value); + } + + return builder.GetSource(); + } + + private ConverterInfo? FindConstructor(INamedTypeSymbol type) + { + ConverterInfo? info = null; + foreach (var ctor in type.Constructors) + { + if (ctor.IsStatic || ctor.DeclaredAccessibility != Accessibility.Public || ctor.Parameters.Length != 1) + { + continue; + } + + var newInfo = new ConverterInfo(); + if (SymbolEqualityComparer.Default.Equals(_readOnlySpanType, ctor.Parameters[0].Type)) + { + newInfo.UseSpan = true; + info = newInfo; + // Won't find a better one + break; + } + else if (!SymbolEqualityComparer.Default.Equals(_stringType, ctor.Parameters[0].Type)) + { + continue; + } + + info = newInfo; + } + + return info; + } + + private ConverterInfo? FindParseMethod(INamedTypeSymbol type) + { + ConverterInfo? info = null; + foreach (var member in type.GetMembers("Parse")) + { + if (!member.IsStatic || member.DeclaredAccessibility != Accessibility.Public || member is not IMethodSymbol method || + method.Parameters.Length < 1 || method.Parameters.Length > 2) + { + continue; + } + + var newInfo = new ConverterInfo() { ParseMethod = true }; + if (SymbolEqualityComparer.Default.Equals(_readOnlySpanType, method.Parameters[0].Type)) + { + newInfo.UseSpan = true; + } + else if (!SymbolEqualityComparer.Default.Equals(_stringType, method.Parameters[0].Type)) + { + continue; + } + + if (method.Parameters.Length == 2) + { + if (_cultureType != null && method.Parameters[1].Type.CanAssignFrom(_cultureType)) + { + newInfo.HasCulture = true; + } + else + { + continue; + } + } + + if (info is not ConverterInfo i || newInfo.IsBetter(i)) + { + info = newInfo; + if (newInfo.HasCulture && newInfo.UseSpan) + { + // Won't find a better one. + break; + } + } + } + + return info; + } + + private static string GenerateName(string displayName) + { + var builder = new StringBuilder(displayName.Length + ConverterSuffix.Length); + foreach (var ch in displayName) + { + builder.Append(char.IsLetterOrDigit(ch) ? ch : '_'); + } + + builder.Append(ConverterSuffix); + return builder.ToString(); + } + + private static void CreateConverter(SourceBuilder builder, INamedTypeSymbol type, ConverterInfo info) + { + if (info.ParseMethod) + { + throw new NotImplementedException(); + } + else + { + CreateConstructorConverter(builder, type, info); + } + } + + private static void CreateConstructorConverter(SourceBuilder builder, INamedTypeSymbol type, ConverterInfo info) + { + builder.AppendLine($"internal class {info.Name} : Ookii.CommandLine.Conversion.ArgumentConverter"); + builder.OpenBlock(); + if (info.UseSpan) + { + builder.AppendLine("public override object? Convert(string value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => Convert(value.AsSpan(), culture, argument);"); + builder.AppendLine(); + builder.AppendLine($"public override object? Convert(System.ReadOnlySpan value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => new {type.ToDisplayString()}(value);"); + } + else + { + builder.AppendLine($"public override object? Convert(string value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => new {type.ToDisplayString()}(value);"); + } + + builder.CloseBlock(); // class + builder.AppendLine(); + } +} diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index e0c5d079..56906a4a 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -7,9 +7,9 @@ namespace Ookii.CommandLine.Generator; internal static class Extensions { - public static bool DerivesFrom(this INamedTypeSymbol symbol, string baseClassName) + public static bool DerivesFrom(this ITypeSymbol symbol, string baseClassName) { - INamedTypeSymbol? current = symbol; + var current = symbol; while (current != null) { if (current.ToDisplayString() == baseClassName) @@ -23,6 +23,22 @@ public static bool DerivesFrom(this INamedTypeSymbol symbol, string baseClassNam return false; } + public static bool DerivesFrom(this ITypeSymbol type, ITypeSymbol baseClass) + { + var current = type; + while (current != null) + { + if (SymbolEqualityComparer.Default.Equals(current, baseClass)) + { + return true; + } + + current = current.BaseType; + } + + return false; + } + public static bool IsNullableValueType(this INamedTypeSymbol symbol) => !symbol.IsReferenceType && symbol.IsGenericType && symbol.ConstructedFrom.ToDisplayString() == "System.Nullable"; @@ -34,7 +50,7 @@ public static INamedTypeSymbol GetUnderlyingType(this INamedTypeSymbol type) public static bool IsEnum(this ITypeSymbol type) => type.BaseType?.ToDisplayString() == "System.Enum"; - public static INamedTypeSymbol? FindGenericInterface(this INamedTypeSymbol symbol, string interfaceName) + public static INamedTypeSymbol? FindGenericInterface(this ITypeSymbol symbol, string interfaceName) { foreach (var iface in symbol.AllInterfaces) { @@ -53,7 +69,7 @@ public static INamedTypeSymbol GetUnderlyingType(this INamedTypeSymbol type) return null; } - public static bool ImplementsInterface(this INamedTypeSymbol symbol, string interfaceName) + public static bool ImplementsInterface(this ITypeSymbol symbol, string interfaceName) { foreach (var iface in symbol.AllInterfaces) { @@ -66,6 +82,23 @@ public static bool ImplementsInterface(this INamedTypeSymbol symbol, string inte return false; } + public static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol interfaceType) + { + foreach (var iface in type.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(iface, interfaceType)) + { + return true; + } + } + + return false; + } + + public static bool CanAssignFrom(this ITypeSymbol targetType, ITypeSymbol sourceType) + => SymbolEqualityComparer.Default.Equals(targetType, sourceType) || sourceType.DerivesFrom(targetType) + || sourceType.ImplementsInterface(targetType); + public static string CreateInstantiation(this AttributeData attribute) { var ctorArgs = attribute.ConstructorArguments.Select(c => c.ToCSharpString()); diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 1e051122..0a53d77e 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -16,18 +16,20 @@ internal class ParserGenerator private readonly SourceProductionContext _context; private readonly INamedTypeSymbol _argumentsClass; private readonly SourceBuilder _builder; + private readonly ConverterGenerator _converterGenerator; - public ParserGenerator(Compilation compilation, SourceProductionContext context, INamedTypeSymbol argumentsClass) + public ParserGenerator(Compilation compilation, SourceProductionContext context, INamedTypeSymbol argumentsClass, ConverterGenerator converterGenerator) { _compilation = compilation; _context = context; _argumentsClass = argumentsClass; _builder = new(argumentsClass.ContainingNamespace); + _converterGenerator = converterGenerator; } - public static string? Generate(Compilation compilation, SourceProductionContext context, INamedTypeSymbol argumentsClass) + public static string? Generate(Compilation compilation, SourceProductionContext context, INamedTypeSymbol argumentsClass, ConverterGenerator converterGenerator) { - var generator = new ParserGenerator(compilation, context, argumentsClass); + var generator = new ParserGenerator(compilation, context, argumentsClass, converterGenerator); return generator.Generate(); } @@ -406,7 +408,7 @@ private static bool CheckAttribute(AttributeData data, string name, ref List()"; } - // TODO: Generate a converter. - return null; + return _converterGenerator.GetConverter(elementType); } } diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index 3cf19bcd..d2d5ba86 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -32,6 +32,7 @@ private static void Execute(Compilation compilation, ImmutableArray"); _builder.AppendLine("#nullable enable"); _builder.AppendLine(); - if (!ns.IsGlobalNamespace) + if (ns != null) { - AppendLine($"namespace {ns.ToDisplayString()}"); + AppendLine($"namespace {ns}"); OpenBlock(); } } diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index d976e73a..bc245eb5 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -35,4 +35,7 @@ partial class Arguments [CommandLineArgument] public int? Test4 { get; set; } + + [CommandLineArgument] + public FileInfo? File { get; set; } } From 053414886545810bd7c11b3fd4688e32a1ada911 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 11 Apr 2023 10:58:38 -0700 Subject: [PATCH 023/234] Generate converters for types with Parse methods. --- .../ConverterGenerator.cs | 19 ++++++------------- src/Samples/TrimTest/Program.cs | 4 ++++ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 08aafd55..07e901c9 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -186,29 +186,22 @@ private static string GenerateName(string displayName) private static void CreateConverter(SourceBuilder builder, INamedTypeSymbol type, ConverterInfo info) { + builder.AppendLine($"internal class {info.Name} : Ookii.CommandLine.Conversion.ArgumentConverter"); + builder.OpenBlock(); + string inputType = info.UseSpan ? "System.ReadOnlySpan" : "string"; + string culture = info.HasCulture ? ", culture" : string.Empty; if (info.ParseMethod) { - throw new NotImplementedException(); + builder.AppendLine($"public override object? Convert({inputType} value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => {type.ToDisplayString()}.Parse(value{culture});"); } else { - CreateConstructorConverter(builder, type, info); + builder.AppendLine($"public override object? Convert({inputType} value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => new {type.ToDisplayString()}(value);"); } - } - private static void CreateConstructorConverter(SourceBuilder builder, INamedTypeSymbol type, ConverterInfo info) - { - builder.AppendLine($"internal class {info.Name} : Ookii.CommandLine.Conversion.ArgumentConverter"); - builder.OpenBlock(); if (info.UseSpan) { builder.AppendLine("public override object? Convert(string value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => Convert(value.AsSpan(), culture, argument);"); - builder.AppendLine(); - builder.AppendLine($"public override object? Convert(System.ReadOnlySpan value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => new {type.ToDisplayString()}(value);"); - } - else - { - builder.AppendLine($"public override object? Convert(string value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => new {type.ToDisplayString()}(value);"); } builder.CloseBlock(); // class diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index bc245eb5..8569e1a4 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -5,6 +5,7 @@ using Ookii.CommandLine.Validation; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Net; var arguments = Arguments.Parse(); if (arguments != null) @@ -38,4 +39,7 @@ partial class Arguments [CommandLineArgument] public FileInfo? File { get; set; } + + [CommandLineArgument] + public IPAddress? Ip { get; set; } } From 1f55223860cd8e298d9e8865fa55ddb9843706b2 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 11 Apr 2023 12:16:47 -0700 Subject: [PATCH 024/234] Fixed interface and array argument handling. --- .../ConverterGenerator.cs | 19 +++-- src/Ookii.CommandLine.Generator/Extensions.cs | 42 +++++++---- .../ParserGenerator.cs | 72 +++++++++++-------- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 3 +- .../Ookii.CommandLine.Tests.csproj | 7 +- .../Conversion/EnumConverter.cs | 15 +++- src/Samples/TrimTest/Program.cs | 5 +- 7 files changed, 109 insertions(+), 54 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 07e901c9..4be80c68 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -48,6 +48,7 @@ public bool IsBetter(ConverterInfo other) private readonly INamedTypeSymbol? _readOnlySpanType; private readonly INamedTypeSymbol? _stringType; private readonly INamedTypeSymbol? _cultureType; + private readonly Dictionary _converters = new(SymbolEqualityComparer.Default); public ConverterGenerator(Compilation compilation) { @@ -60,9 +61,7 @@ public ConverterGenerator(Compilation compilation) } } - private readonly Dictionary _converters = new(SymbolEqualityComparer.Default); - - public string? GetConverter(INamedTypeSymbol type) + public string? GetConverter(ITypeSymbol type) { if (_converters.TryGetValue(type, out var converter)) { @@ -96,10 +95,15 @@ public ConverterGenerator(Compilation compilation) return builder.GetSource(); } - private ConverterInfo? FindConstructor(INamedTypeSymbol type) + private ConverterInfo? FindConstructor(ITypeSymbol type) { + if (type is not INamedTypeSymbol namedType) + { + return null; + } + ConverterInfo? info = null; - foreach (var ctor in type.Constructors) + foreach (var ctor in namedType.Constructors) { if (ctor.IsStatic || ctor.DeclaredAccessibility != Accessibility.Public || ctor.Parameters.Length != 1) { @@ -125,7 +129,7 @@ public ConverterGenerator(Compilation compilation) return info; } - private ConverterInfo? FindParseMethod(INamedTypeSymbol type) + private ConverterInfo? FindParseMethod(ITypeSymbol type) { ConverterInfo? info = null; foreach (var member in type.GetMembers("Parse")) @@ -184,8 +188,9 @@ private static string GenerateName(string displayName) return builder.ToString(); } - private static void CreateConverter(SourceBuilder builder, INamedTypeSymbol type, ConverterInfo info) + private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, ConverterInfo info) { + // TODO: Handle exceptions similar to reflection versions. builder.AppendLine($"internal class {info.Name} : Ookii.CommandLine.Conversion.ArgumentConverter"); builder.OpenBlock(); string inputType = info.UseSpan ? "System.ReadOnlySpan" : "string"; diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 56906a4a..53f949bc 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -1,7 +1,9 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using System; +using System.Diagnostics; using System.Text; +using static System.Net.Mime.MediaTypeNames; namespace Ookii.CommandLine.Generator; @@ -39,28 +41,33 @@ public static bool DerivesFrom(this ITypeSymbol type, ITypeSymbol baseClass) return false; } - public static bool IsNullableValueType(this INamedTypeSymbol symbol) - => !symbol.IsReferenceType && symbol.IsGenericType && symbol.ConstructedFrom.ToDisplayString() == "System.Nullable"; + public static bool IsNullableValueType(this INamedTypeSymbol type) + => !type.IsReferenceType && type.IsGenericType && type.ConstructedFrom.ToDisplayString() == "System.Nullable"; - public static bool AllowsNull(this INamedTypeSymbol type) - => type.IsNullableValueType() || (type.IsReferenceType && type.NullableAnnotation != NullableAnnotation.NotAnnotated); + public static bool IsNullableValueType(this ITypeSymbol type) + => type is INamedTypeSymbol namedType && namedType.IsNullableValueType(); + + public static bool AllowsNull(this ITypeSymbol type) + => (type is not INamedTypeSymbol namedType || namedType.IsNullableValueType()) || (type.IsReferenceType && type.NullableAnnotation != NullableAnnotation.NotAnnotated); public static INamedTypeSymbol GetUnderlyingType(this INamedTypeSymbol type) => type.IsNullableValueType() ? (INamedTypeSymbol)type.TypeArguments[0] : type; + public static ITypeSymbol GetUnderlyingType(this ITypeSymbol type) + => type is INamedTypeSymbol namedType && namedType.IsNullableValueType() ? (INamedTypeSymbol)namedType.TypeArguments[0] : type; + public static bool IsEnum(this ITypeSymbol type) => type.BaseType?.ToDisplayString() == "System.Enum"; - public static INamedTypeSymbol? FindGenericInterface(this ITypeSymbol symbol, string interfaceName) + public static INamedTypeSymbol? FindGenericInterface(this ITypeSymbol type, string interfaceName) { - foreach (var iface in symbol.AllInterfaces) + if (type.TypeKind == TypeKind.Interface && ((INamedTypeSymbol)type).IsTypeOrConstructedFrom(interfaceName)) { - var realIface = iface; - if (iface.IsGenericType) - { - realIface = iface.ConstructedFrom; - } + return (INamedTypeSymbol)type; + } - if (realIface.ToDisplayString() == interfaceName) + foreach (var iface in type.AllInterfaces) + { + if (iface.IsTypeOrConstructedFrom(interfaceName)) { return iface; } @@ -69,6 +76,17 @@ public static INamedTypeSymbol GetUnderlyingType(this INamedTypeSymbol type) return null; } + public static bool IsTypeOrConstructedFrom(this INamedTypeSymbol type, string name) + { + var realType = type; + if (realType.IsGenericType) + { + realType = realType.ConstructedFrom; + } + + return realType.ToDisplayString() == name; + } + public static bool ImplementsInterface(this ITypeSymbol symbol, string interfaceName) { foreach (var iface in symbol.AllInterfaces) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 0a53d77e..4d5da9ef 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -107,7 +107,6 @@ private void GenerateProvider() _builder.AppendLine("public override System.Collections.Generic.IEnumerable GetArguments(Ookii.CommandLine.CommandLineParser parser)"); _builder.OpenBlock(); - //Debugger.Launch(); foreach (var member in _argumentsClass.GetMembers()) { GenerateArgument(member); @@ -172,18 +171,29 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) throw new NotImplementedException(); } - var originalArgumentType = (INamedTypeSymbol)property!.Type; - var argumentType = (INamedTypeSymbol)originalArgumentType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - string extra = string.Empty; + if (property == null || property.IsStatic) + { + return; + } + + var originalArgumentType = property!.Type; + var argumentType = originalArgumentType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + var notNullAnnotation = string.Empty; var allowsNull = originalArgumentType.AllowsNull(); - if (!allowsNull) + if (allowsNull) + { + // Needed in case the original definition was in a context without NRT support. + originalArgumentType = originalArgumentType.WithNullableAnnotation(NullableAnnotation.Annotated); + } + else { - extra = "!"; + notNullAnnotation = "!"; } - INamedTypeSymbol elementTypeWithNullable = argumentType; - INamedTypeSymbol? keyType = null; - INamedTypeSymbol? valueType = null; + ITypeSymbol elementTypeWithNullable = argumentType; + var namedElementTypeWithNullable = elementTypeWithNullable as INamedTypeSymbol; + ITypeSymbol? keyType = null; + ITypeSymbol? valueType = null; if (keyValueSeparator != null) { _builder.AppendLine($"var keyValueSeparatorAttribute{member.Name} = {keyValueSeparator.CreateInstantiation()};"); @@ -204,11 +214,12 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) Debug.Assert(multiValueElementType != null); kind = "Ookii.CommandLine.ArgumentKind.Dictionary"; elementTypeWithNullable = multiValueElementType!; - keyType = (INamedTypeSymbol)elementTypeWithNullable.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.NotAnnotated); - var rawValueType = ((INamedTypeSymbol)elementTypeWithNullable.TypeArguments[1]); + // 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 = (INamedTypeSymbol)rawValueType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - // TODO: Converter + valueType = rawValueType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); if (converterAttribute == null) { var keyConverter = DetermineConverter(keyType.GetUnderlyingType(), keyConverterAttribute, keyType.IsNullableValueType()); @@ -237,6 +248,7 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) Debug.Assert(multiValueElementType != null); kind = "Ookii.CommandLine.ArgumentKind.MultiValue"; elementTypeWithNullable = multiValueElementType!; + namedElementTypeWithNullable = elementTypeWithNullable as INamedTypeSymbol; allowsNull = elementTypeWithNullable.AllowsNull(); } } @@ -245,9 +257,8 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) kind = "Ookii.CommandLine.ArgumentKind.Method"; } - var elementType = elementTypeWithNullable; - elementType = elementTypeWithNullable.GetUnderlyingType(); - converter ??= DetermineConverter(elementType, converterAttribute, elementTypeWithNullable.IsNullableValueType()); + var elementType = namedElementTypeWithNullable?.GetUnderlyingType() ?? elementTypeWithNullable; + converter ??= DetermineConverter(elementType, converterAttribute, ((INamedTypeSymbol)elementTypeWithNullable).IsNullableValueType()); if (converter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, elementType)); @@ -315,7 +326,7 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) if (property?.SetMethod?.DeclaredAccessibility == Accessibility.Public) { - _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.Name})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{extra}"); + _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.Name})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"); } if (property != null) @@ -352,15 +363,18 @@ private static bool CheckAttribute(AttributeData data, string name, ref List it doesn't matter if the property is - // read-only or not. - if (argumentType.IsGenericType && argumentType.ConstructedFrom.ToDisplayString() == "System.Collections.Generic.Dictionary") + if (argumentType is INamedTypeSymbol namedType) { - var keyValuePair = _compilation.GetTypeByMetadataName(typeof(KeyValuePair<,>).FullName)!; - var elementType = keyValuePair.Construct(argumentType.TypeArguments, argumentType.TypeArgumentNullableAnnotations); - return (null, argumentType, elementType); + // If the type is Dictionary it doesn't matter if the property is + // read-only or not. + if (namedType.IsGenericType && namedType.ConstructedFrom.ToDisplayString() == "System.Collections.Generic.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) @@ -377,7 +391,7 @@ private static bool CheckAttribute(AttributeData data, string name, ref List"); + var collectionType = argumentType.FindGenericInterface("System.Collections.Generic.ICollection"); if (collectionType != null) { - var elementType = (INamedTypeSymbol)collectionType.TypeArguments[0]; + var elementType = collectionType.TypeArguments[0]; return (collectionType, null, elementType); } @@ -408,7 +422,7 @@ private static bool CheckAttribute(AttributeData data, string name, ref List _arg12 = new Collection(); private readonly Dictionary _arg14 = new Dictionary(); diff --git a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj index 2d182c52..0a0c84f9 100644 --- a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj +++ b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj @@ -1,4 +1,4 @@ - + net7.0;net6.0;net48 @@ -6,6 +6,7 @@ Tests for Ookii.CommandLine. false 9.0 + true @@ -20,6 +21,8 @@ + - diff --git a/src/Ookii.CommandLine/Conversion/EnumConverter.cs b/src/Ookii.CommandLine/Conversion/EnumConverter.cs index b0032b78..b324c8e6 100644 --- a/src/Ookii.CommandLine/Conversion/EnumConverter.cs +++ b/src/Ookii.CommandLine/Conversion/EnumConverter.cs @@ -3,15 +3,25 @@ namespace Ookii.CommandLine.Conversion; -internal class EnumConverter : ArgumentConverter +/// +/// A converter for arguments with enumeration values. +/// +/// +public class EnumConverter : ArgumentConverter { private readonly Type _enumType; + /// + /// Initializes a new instance of the for the specified enumeration + /// type. + /// + /// The enumeration type. public EnumConverter(Type enumType) { - _enumType = enumType; + _enumType = enumType ?? throw new ArgumentNullException(nameof(enumType)); } + /// public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) { try @@ -29,6 +39,7 @@ public EnumConverter(Type enumType) } #if NET6_0_OR_GREATER + /// public override object? Convert(ReadOnlySpan value, CultureInfo culture, CommandLineArgument argument) { try diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 8569e1a4..c2ac2adf 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -38,8 +38,11 @@ partial class Arguments public int? Test4 { get; set; } [CommandLineArgument] - public FileInfo? File { get; set; } + public FileInfo[]? File { get; set; } [CommandLineArgument] public IPAddress? Ip { get; set; } + + [CommandLineArgument] + public IDictionary Arg14 { get; } = new SortedDictionary(); } From 7e48a18cbfe83aef19d3ecc37d24f52efc9613d0 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 11 Apr 2023 14:54:08 -0700 Subject: [PATCH 025/234] Run tests against generated parsers. --- .../ConverterGenerator.cs | 2 +- src/Ookii.CommandLine.Generator/Extensions.cs | 13 +- .../ParserGenerator.cs | 2 +- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 59 ++- .../CommandLineParserTest.Usage.cs | 2 +- .../CommandLineParserTest.cs | 352 ++++++++++-------- src/Ookii.CommandLine.Tests/CommandTypes.cs | 2 +- src/Ookii.CommandLine/CommandLineArgument.cs | 4 + src/Ookii.CommandLine/CommandLineParser.cs | 23 +- .../Support/ArgumentProvider.cs | 8 + .../Support/ArgumentProviderKind.cs | 20 + .../Support/GeneratedArgument.cs | 19 +- .../Support/GeneratedArgumentProvider.cs | 7 +- .../Support/ReflectionArgumentProvider.cs | 17 +- 14 files changed, 324 insertions(+), 206 deletions(-) create mode 100644 src/Ookii.CommandLine/Support/ArgumentProviderKind.cs diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 4be80c68..04435eeb 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -206,7 +206,7 @@ private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, Con if (info.UseSpan) { - builder.AppendLine("public override object? Convert(string value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => Convert(value.AsSpan(), culture, argument);"); + builder.AppendLine("public override object? Convert(string value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => Convert(System.MemoryExtensions.AsSpan(value), culture, argument);"); } builder.CloseBlock(); // class diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 53f949bc..a889dd67 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -119,8 +119,8 @@ public static bool CanAssignFrom(this ITypeSymbol targetType, ITypeSymbol source public static string CreateInstantiation(this AttributeData attribute) { - var ctorArgs = attribute.ConstructorArguments.Select(c => c.ToCSharpString()); - var namedArgs = attribute.NamedArguments.Select(n => $"{n.Key} = {n.Value.ToCSharpString()}"); + var ctorArgs = attribute.ConstructorArguments.Select(c => c.ToFullCSharpString()); + var namedArgs = attribute.NamedArguments.Select(n => $"{n.Key} = {n.Value.ToFullCSharpString()}"); return $"new {attribute.AttributeClass?.ToDisplayString()}({string.Join(", ", ctorArgs)}) {{ {string.Join(", ", namedArgs)} }}"; } @@ -128,4 +128,13 @@ public static string ToCSharpString(this bool value) { return value ? "true" : "false"; } + + public static string ToFullCSharpString(this TypedConstant constant) + { + return constant.Kind switch + { + TypedConstantKind.Array => $"new {constant.Type?.ToDisplayString()} {constant.ToCSharpString()}", + _ => constant.ToCSharpString(), + }; + } } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 4d5da9ef..058754c0 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -190,7 +190,7 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) notNullAnnotation = "!"; } - ITypeSymbol elementTypeWithNullable = argumentType; + var elementTypeWithNullable = argumentType; var namedElementTypeWithNullable = elementTypeWithNullable as INamedTypeSymbol; ITypeSymbol? keyType = null; ITypeSymbol? valueType = null; diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 4b5cde5b..11641679 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -10,7 +10,8 @@ namespace Ookii.CommandLine.Tests { - class EmptyArguments + [GeneratedParser] + partial class EmptyArguments { } @@ -94,7 +95,8 @@ public IDictionary Arg14 public static string NotAnArg3 { get; set; } } - class ThrowingArguments + [GeneratedParser] + partial class ThrowingArguments { private int _throwingArgument; @@ -114,7 +116,8 @@ public int ThrowingArgument } } - class ThrowingConstructor + [GeneratedParser] + partial class ThrowingConstructor { public ThrowingConstructor() { @@ -125,7 +128,8 @@ public ThrowingConstructor() public int Arg { get; set; } } - class DictionaryArguments + [GeneratedParser] + partial class DictionaryArguments { [CommandLineArgument] public Dictionary NoDuplicateKeys { get; set; } @@ -133,7 +137,8 @@ class DictionaryArguments public Dictionary DuplicateKeys { get; set; } } - class MultiValueSeparatorArguments + [GeneratedParser] + partial class MultiValueSeparatorArguments { [CommandLineArgument] public string[] NoSeparator { get; set; } @@ -141,7 +146,8 @@ class MultiValueSeparatorArguments public string[] Separator { get; set; } } - class SimpleArguments + [GeneratedParser] + partial class SimpleArguments { [CommandLineArgument] public string Argument1 { get; set; } @@ -149,7 +155,8 @@ class SimpleArguments public string Argument2 { get; set; } } - class KeyValueSeparatorArguments + [GeneratedParser] + partial class KeyValueSeparatorArguments { [CommandLineArgument] public Dictionary DefaultSeparator { get; set; } @@ -159,7 +166,8 @@ class KeyValueSeparatorArguments public Dictionary CustomSeparator { get; set; } } - class CancelArguments + [GeneratedParser] + partial class CancelArguments { [CommandLineArgument] public string Argument1 { get; set; } @@ -174,6 +182,7 @@ class CancelArguments public bool DoesCancel { get; set; } } + [GeneratedParser] [ParseOptions( Mode = ParsingMode.LongShort, DuplicateArguments = ErrorMode.Allow, @@ -183,20 +192,22 @@ class CancelArguments CaseSensitive = true, NameValueSeparator = '=', AutoHelpArgument = false)] - class ParseOptionsArguments + partial class ParseOptionsArguments { [CommandLineArgument] public string Argument { get; set; } } - class CultureArguments + [GeneratedParser] + partial class CultureArguments { [CommandLineArgument] public float Argument { get; set; } } + [GeneratedParser] [ParseOptions(Mode = ParsingMode.LongShort)] - class LongShortArguments + partial class LongShortArguments { [CommandLineArgument, ShortAlias('c')] [Description("Arg1 description.")] @@ -302,7 +313,8 @@ public static void NotAnArgument() } } - class AutomaticConflictingNameArguments + [GeneratedParser] + partial class AutomaticConflictingNameArguments { [CommandLineArgument] public int Help { get; set; } @@ -311,14 +323,16 @@ class AutomaticConflictingNameArguments public int Version { get; set; } } + [GeneratedParser] [ParseOptions(Mode = ParsingMode.LongShort)] - class AutomaticConflictingShortNameArguments + partial class AutomaticConflictingShortNameArguments { [CommandLineArgument(ShortName = '?')] public int Foo { get; set; } } - class HiddenArguments + [GeneratedParser] + partial class HiddenArguments { [CommandLineArgument] public int Foo { get; set; } @@ -327,7 +341,8 @@ class HiddenArguments public int Hidden { get; set; } } - class NameTransformArguments + [GeneratedParser] + partial class NameTransformArguments { [CommandLineArgument(Position = 0, IsRequired = true)] public string testArg { get; set; } @@ -342,7 +357,8 @@ class NameTransformArguments public int Explicit { get; set; } } - class ValueDescriptionTransformArguments + [GeneratedParser] + partial class ValueDescriptionTransformArguments { [CommandLineArgument] public FileInfo Arg1 { get; set; } @@ -396,9 +412,10 @@ public static void Arg3(int value) public int? NotNull { get; set; } } + [GeneratedParser] // N.B. nameof is only safe if the argument name matches the property name. [RequiresAny(nameof(Address), nameof(Path))] - class DependencyArguments + partial class DependencyArguments { [CommandLineArgument] [Description("The address.")] @@ -424,7 +441,8 @@ class DependencyArguments public FileInfo Path { get; set; } } - class MultiValueWhiteSpaceArguments + [GeneratedParser] + partial class MultiValueWhiteSpaceArguments { [CommandLineArgument(Position = 0)] @@ -522,13 +540,14 @@ public StructWithCtor(string value) public int Value { get; set; } } - class ConversionArguments + [GeneratedParser] + partial class ConversionArguments { [CommandLineArgument] public StructWithParseCulture ParseCulture { get; set; } [CommandLineArgument] - public StructWithParse Parse { get; set; } + public StructWithParse ParseStruct { get; set; } [CommandLineArgument] public StructWithCtor Ctor { get; set; } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs index f7c1cf48..d9ce0a88 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs @@ -1,6 +1,6 @@ namespace Ookii.CommandLine.Tests { - public partial class CommandLineParserTest + partial class CommandLineParserTest { private const string _executableName = "test"; diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index a02b0c9c..22dd5a11 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1,5 +1,6 @@ // Copyright (c) Sven Groot (Ookii.org) using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Support; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -17,14 +18,12 @@ namespace Ookii.CommandLine.Tests [TestClass()] public partial class CommandLineParserTest { - /// - ///A test for CommandLineParser Constructor - /// - [TestMethod()] - public void ConstructorEmptyArgumentsTest() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ConstructorEmptyArgumentsTest(ArgumentProviderKind kind) { Type argumentsType = typeof(EmptyArguments); - CommandLineParser target = new CommandLineParser(argumentsType); + var target = CreateParser(kind); Assert.AreEqual(CultureInfo.InvariantCulture, target.Culture); Assert.AreEqual(false, target.AllowDuplicateArguments); Assert.AreEqual(true, target.AllowWhiteSpaceValueSeparator); @@ -36,18 +35,19 @@ public void ConstructorEmptyArgumentsTest() Assert.AreEqual(string.Empty, target.Description); Assert.AreEqual(2, target.Arguments.Count); using var args = target.Arguments.GetEnumerator(); - TestArguments(target.Arguments, new[] + VerifyArguments(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 }, }); } - [TestMethod()] - public void ConstructorTest() + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ConstructorTest(ArgumentProviderKind kind) { Type argumentsType = typeof(TestArguments); - CommandLineParser target = new CommandLineParser(argumentsType); + var target = CreateParser(kind); Assert.AreEqual(CultureInfo.InvariantCulture, target.Culture); Assert.AreEqual(false, target.AllowDuplicateArguments); Assert.AreEqual(true, target.AllowWhiteSpaceValueSeparator); @@ -58,7 +58,7 @@ public void ConstructorTest() Assert.AreEqual("Friendly name", target.ApplicationFriendlyName); Assert.AreEqual("Test arguments description.", target.Description); Assert.AreEqual(18, target.Arguments.Count); - TestArguments(target.Arguments, new[] + VerifyArguments(target.Arguments, new[] { 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" }, @@ -82,10 +82,10 @@ public void ConstructorTest() } [TestMethod] - public void ParseTest() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTest(ArgumentProviderKind kind) { - var target = new CommandLineParser(); - + 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. @@ -111,45 +111,29 @@ public void ParseTest() } [TestMethod] - public void ParseTestEmptyArguments() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestEmptyArguments(ArgumentProviderKind kind) { - Type argumentsType = typeof(EmptyArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; - - var target = new CommandLineParser(argumentsType, options); - + 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.Parse(new[] { "Foo", "Bar" }), target, CommandLineArgumentErrorCategory.TooManyArguments); } [TestMethod] - public void ParseTestTooManyArguments() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestTooManyArguments(ArgumentProviderKind kind) { - Type argumentsType = typeof(ThrowingArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; - - var target = new CommandLineParser(argumentsType, options); + var target = CreateParser(kind); // Only accepts one positional argument. CheckThrows(() => target.Parse(new[] { "Foo", "Bar" }), target, CommandLineArgumentErrorCategory.TooManyArguments); } [TestMethod] - public void ParseTestPropertySetterThrows() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestPropertySetterThrows(ArgumentProviderKind kind) { - Type argumentsType = typeof(ThrowingArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; - - var target = new CommandLineParser(argumentsType, options); + var target = CreateParser(kind); CheckThrows(() => target.Parse(new[] { "-ThrowingArgument", "-5" }), target, @@ -159,15 +143,10 @@ public void ParseTestPropertySetterThrows() } [TestMethod] - public void ParseTestConstructorThrows() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestConstructorThrows(ArgumentProviderKind kind) { - Type argumentsType = typeof(ThrowingConstructor); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; - - var target = new CommandLineParser(argumentsType, options); + var target = CreateParser(kind); CheckThrows(() => target.Parse(Array.Empty()), target, @@ -177,17 +156,12 @@ public void ParseTestConstructorThrows() } [TestMethod] - public void ParseTestDuplicateDictionaryKeys() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestDuplicateDictionaryKeys(ArgumentProviderKind kind) { - Type argumentsType = typeof(DictionaryArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; - - var target = new CommandLineParser(argumentsType, options); + var target = CreateParser(kind); - DictionaryArguments args = (DictionaryArguments)target.Parse(new[] { "-DuplicateKeys", "Foo=1", "-DuplicateKeys", "Bar=2", "-DuplicateKeys", "Foo=3" }); + DictionaryArguments 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"]); @@ -201,34 +175,24 @@ public void ParseTestDuplicateDictionaryKeys() } [TestMethod] - public void ParseTestMultiValueSeparator() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestMultiValueSeparator(ArgumentProviderKind kind) { - Type argumentsType = typeof(MultiValueSeparatorArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; + var target = CreateParser(kind); - var target = new CommandLineParser(argumentsType, options); - - MultiValueSeparatorArguments args = (MultiValueSeparatorArguments)target.Parse(new[] { "-NoSeparator", "Value1,Value2", "-NoSeparator", "Value3", "-Separator", "Value1,Value2", "-Separator", "Value3" }); + MultiValueSeparatorArguments 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] - public void ParseTestNameValueSeparator() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestNameValueSeparator(ArgumentProviderKind kind) { - Type argumentsType = typeof(SimpleArguments); - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; - - var target = new CommandLineParser(argumentsType, options); + var target = CreateParser(kind); Assert.AreEqual(CommandLineParser.DefaultNameValueSeparator, target.NameValueSeparator); - SimpleArguments args = (SimpleArguments)target.Parse(new[] { "-Argument1:test", "-Argument2:foo:bar" }); + SimpleArguments args = target.Parse(new[] { "-Argument1:test", "-Argument2:foo:bar" }); Assert.IsNotNull(args); Assert.AreEqual("test", args.Argument1); Assert.AreEqual("foo:bar", args.Argument2); @@ -238,7 +202,7 @@ public void ParseTestNameValueSeparator() "Argument1=test"); target.Options.NameValueSeparator = '='; - args = (SimpleArguments)target.Parse(new[] { "-Argument1=test", "-Argument2=foo=bar" }); + args = target.Parse(new[] { "-Argument1=test", "-Argument2=foo=bar" }); Assert.IsNotNull(args); Assert.AreEqual("test", args.Argument1); Assert.AreEqual("foo=bar", args.Argument2); @@ -249,9 +213,10 @@ public void ParseTestNameValueSeparator() } [TestMethod] - public void ParseTestKeyValueSeparator() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestKeyValueSeparator(ArgumentProviderKind kind) { - var target = new CommandLineParser(typeof(KeyValueSeparatorArguments)); + var target = CreateParser(kind); Assert.AreEqual("=", target.GetArgument("DefaultSeparator")!.KeyValueSeparator); Assert.AreEqual("String=Int32", target.GetArgument("DefaultSeparator")!.ValueDescription); Assert.AreEqual("<=>", target.GetArgument("CustomSeparator")!.KeyValueSeparator); @@ -276,15 +241,15 @@ public void ParseTestKeyValueSeparator() } [TestMethod] - public void TestWriteUsage() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsage(ArgumentProviderKind kind) { - Type argumentsType = typeof(TestArguments); var options = new ParseOptions() { ArgumentNamePrefixes = new[] { "/", "-" } }; - var target = new CommandLineParser(argumentsType, options); + var target = CreateParser(kind, options); var writer = new UsageWriter() { ExecutableName = _executableName @@ -295,9 +260,10 @@ public void TestWriteUsage() } [TestMethod] - public void TestWriteUsageLongShort() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageLongShort(ArgumentProviderKind kind) { - var target = new CommandLineParser(); + var target = CreateParser(kind); var options = new UsageWriter() { ExecutableName = _executableName @@ -321,9 +287,10 @@ public void TestWriteUsageLongShort() } [TestMethod] - public void TestWriteUsageFilter() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageFilter(ArgumentProviderKind kind) { - var target = new CommandLineParser(); + var target = CreateParser(kind); var options = new UsageWriter() { ExecutableName = _executableName, @@ -343,14 +310,15 @@ public void TestWriteUsageFilter() } [TestMethod] - public void TestWriteUsageColor() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageColor(ArgumentProviderKind kind) { var options = new ParseOptions() { ArgumentNamePrefixes = new[] { "/", "-" } }; - var target = new CommandLineParser(typeof(TestArguments), options); + CommandLineParser target = CreateParser(kind, options); var writer = new UsageWriter(useColor: true) { ExecutableName = _executableName, @@ -359,15 +327,16 @@ public void TestWriteUsageColor() string actual = target.GetUsage(writer); Assert.AreEqual(_expectedUsageColor, actual); - target = new CommandLineParser(typeof(LongShortArguments)); + target = CreateParser(kind); actual = target.GetUsage(writer); Assert.AreEqual(_expectedLongShortUsageColor, actual); } [TestMethod] - public void TestWriteUsageOrder() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageOrder(ArgumentProviderKind kind) { - var parser = new CommandLineParser(); + var parser = CreateParser(kind); var options = new UsageWriter() { ExecutableName = _executableName, @@ -389,7 +358,7 @@ public void TestWriteUsageOrder() usage = parser.GetUsage(options); Assert.AreEqual(_expectedUsageAlphabeticalShortNameDescending, usage); - parser = new CommandLineParser(new ParseOptions() { Mode = ParsingMode.Default }); + parser = CreateParser(kind, new ParseOptions() { Mode = ParsingMode.Default }); options.ArgumentDescriptionListOrder = DescriptionListSortMode.Alphabetical; usage = parser.GetUsage(options); Assert.AreEqual(_expectedUsageAlphabetical, usage); @@ -409,7 +378,8 @@ public void TestWriteUsageOrder() } [TestMethod] - public void TestWriteUsageSeparator() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageSeparator(ArgumentProviderKind kind) { var options = new ParseOptions() { @@ -420,13 +390,14 @@ public void TestWriteUsageSeparator() UseWhiteSpaceValueSeparator = false, } }; - var target = new CommandLineParser(options); + var target = CreateParser(kind, options); string actual = target.GetUsage(options.UsageWriter); Assert.AreEqual(_expectedUsageSeparator, actual); } [TestMethod] - public void TestWriteUsageCustomIndent() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageCustomIndent(ArgumentProviderKind kind) { var options = new ParseOptions() { @@ -436,13 +407,14 @@ public void TestWriteUsageCustomIndent() ArgumentDescriptionIndent = 4, } }; - var target = new CommandLineParser(options); + var target = CreateParser(kind, options); string actual = target.GetUsage(options.UsageWriter); Assert.AreEqual(_expectedCustomIndentUsage, actual); } [TestMethod] - public void TestStaticParse() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestStaticParse(ArgumentProviderKind kind) { using var output = new StringWriter(); using var lineWriter = new LineWrappingTextWriter(output, 0); @@ -457,21 +429,21 @@ public void TestStaticParse() } }; - var result = CommandLineParser.Parse(new[] { "foo", "-Arg6", "bar" }, options); + 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 = CommandLineParser.Parse(Array.Empty(), options); + 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 = CommandLineParser.Parse(new[] { "-Help" }, options); + result = StaticParse(kind, new[] { "-Help" }, options); Assert.IsNull(result); Assert.AreEqual(0, error.ToString().Length); Assert.AreEqual(_expectedDefaultUsage, output.ToString()); @@ -479,7 +451,7 @@ public void TestStaticParse() options.ShowUsageOnError = UsageHelpRequest.SyntaxOnly; output.GetStringBuilder().Clear(); error.GetStringBuilder().Clear(); - result = CommandLineParser.Parse(Array.Empty(), options); + result = StaticParse(kind, Array.Empty(), options); Assert.IsNull(result); Assert.IsTrue(error.ToString().Length > 0); Assert.AreEqual(_expectedUsageSyntaxOnly, output.ToString()); @@ -487,7 +459,7 @@ public void TestStaticParse() options.ShowUsageOnError = UsageHelpRequest.None; output.GetStringBuilder().Clear(); error.GetStringBuilder().Clear(); - result = CommandLineParser.Parse(Array.Empty(), options); + result = StaticParse(kind, Array.Empty(), options); Assert.IsNull(result); Assert.IsTrue(error.ToString().Length > 0); Assert.AreEqual(_expectedUsageMessageOnly, output.ToString()); @@ -495,19 +467,20 @@ public void TestStaticParse() // Still get full help with -Help arg. output.GetStringBuilder().Clear(); error.GetStringBuilder().Clear(); - result = CommandLineParser.Parse(new[] { "-Help" }, options); + result = StaticParse(kind, new[] { "-Help" }, options); Assert.IsNull(result); Assert.AreEqual(0, error.ToString().Length); Assert.AreEqual(_expectedDefaultUsage, output.ToString()); } [TestMethod] - public void TestCancelParsing() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCancelParsing(ArgumentProviderKind kind) { - var parser = new CommandLineParser(typeof(CancelArguments)); + var parser = CreateParser(kind); // Don't cancel if -DoesCancel not specified. - var result = (CancelArguments)parser.Parse(new[] { "-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar" }); + var result = parser.Parse(new[] { "-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar" }); Assert.IsNotNull(result); Assert.IsFalse(parser.HelpRequested); Assert.IsTrue(result.DoesNotCancel); @@ -516,7 +489,7 @@ public void TestCancelParsing() Assert.AreEqual("bar", result.Argument2); // Cancel if -DoesCancel specified. - result = (CancelArguments)parser.Parse(new[] { "-Argument1", "foo", "-DoesCancel", "-Argument2", "bar" }); + result = parser.Parse(new[] { "-Argument1", "foo", "-DoesCancel", "-Argument2", "bar" }); Assert.IsNull(result); Assert.IsTrue(parser.HelpRequested); Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); @@ -541,7 +514,7 @@ static void handler1(object sender, ArgumentParsedEventArgs e) } parser.ArgumentParsed += handler1; - result = (CancelArguments)parser.Parse(new[] { "-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar" }); + result = parser.Parse(new[] { "-Argument1", "foo", "-DoesNotCancel", "-Argument2", "bar" }); Assert.IsNull(result); Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.LastException); @@ -567,7 +540,7 @@ static void handler2(object sender, ArgumentParsedEventArgs e) } parser.ArgumentParsed += handler2; - result = (CancelArguments)parser.Parse(new[] { "-Argument1", "foo", "-DoesCancel", "-Argument2", "bar" }); + result = parser.Parse(new[] { "-Argument1", "foo", "-DoesCancel", "-Argument2", "bar" }); Assert.IsNotNull(result); Assert.IsFalse(parser.HelpRequested); Assert.IsFalse(result.DoesNotCancel); @@ -576,7 +549,7 @@ static void handler2(object sender, ArgumentParsedEventArgs e) Assert.AreEqual("bar", result.Argument2); // Automatic help argument should cancel. - result = (CancelArguments)parser.Parse(new[] { "-Help" }); + result = parser.Parse(new[] { "-Help" }); Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.LastException); Assert.AreEqual("Help", parser.ParseResult.ArgumentName); @@ -585,9 +558,10 @@ static void handler2(object sender, ArgumentParsedEventArgs e) } [TestMethod] - public void TestParseOptionsAttribute() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestParseOptionsAttribute(ArgumentProviderKind kind) { - var parser = new CommandLineParser(typeof(ParseOptionsArguments)); + var parser = CreateParser(kind); Assert.IsFalse(parser.AllowWhiteSpaceValueSeparator); Assert.IsTrue(parser.AllowDuplicateArguments); Assert.AreEqual('=', parser.NameValueSeparator); @@ -612,7 +586,7 @@ public void TestParseOptionsAttribute() AutoHelpArgument = true, }; - parser = new CommandLineParser(typeof(ParseOptionsArguments), options); + parser = CreateParser(kind, options); Assert.IsTrue(parser.AllowWhiteSpaceValueSeparator); Assert.IsFalse(parser.AllowDuplicateArguments); Assert.AreEqual(';', parser.NameValueSeparator); @@ -627,30 +601,32 @@ public void TestParseOptionsAttribute() } [TestMethod] - public void TestCulture() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCulture(ArgumentProviderKind kind) { - var result = CommandLineParser.Parse(new[] { "-Argument", "5.5" }); + var result = StaticParse(kind, new[] { "-Argument", "5.5" }); Assert.IsNotNull(result); Assert.AreEqual(5.5, result.Argument); - result = CommandLineParser.Parse(new[] { "-Argument", "5,5" }); + 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 = CommandLineParser.Parse(new[] { "-Argument", "5,5" }, options); + result = StaticParse(kind, new[] { "-Argument", "5,5" }, options); Assert.IsNotNull(result); Assert.AreEqual(5.5, result.Argument); - result = CommandLineParser.Parse(new[] { "-Argument", "5,5" }); + result = StaticParse(kind, new[] { "-Argument", "5,5" }); Assert.IsNotNull(result); // . was interpreted as a thousands separator. Assert.AreEqual(55, result.Argument); } [TestMethod] - public void TestLongShortMode() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestLongShortMode(ArgumentProviderKind kind) { - var parser = new CommandLineParser(); + var parser = CreateParser(kind); Assert.AreEqual(ParsingMode.LongShort, parser.Mode); Assert.AreEqual(CommandLineParser.DefaultLongArgumentNamePrefix, parser.LongArgumentNamePrefix); CollectionAssert.AreEqual(CommandLineParser.GetDefaultArgumentNamePrefixes(), parser.ArgumentNamePrefixes); @@ -750,23 +726,25 @@ public void TestMethodArguments() } [TestMethod] - public void TestAutomaticArgumentConflict() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestAutomaticArgumentConflict(ArgumentProviderKind kind) { - var parser = new CommandLineParser(typeof(AutomaticConflictingNameArguments)); - TestArgument(parser.GetArgument("Help"), new ExpectedArgument("Help", typeof(int))); - TestArgument(parser.GetArgument("Version"), new ExpectedArgument("Version", typeof(int))); + CommandLineParser parser = CreateParser(kind); + VerifyArgument(parser.GetArgument("Help"), new ExpectedArgument("Help", typeof(int))); + VerifyArgument(parser.GetArgument("Version"), new ExpectedArgument("Version", typeof(int))); - parser = new CommandLineParser(typeof(AutomaticConflictingShortNameArguments)); - TestArgument(parser.GetShortArgument('?'), new ExpectedArgument("Foo", typeof(int)) { ShortName = '?' }); + parser = CreateParser(kind); + VerifyArgument(parser.GetShortArgument('?'), new ExpectedArgument("Foo", typeof(int)) { ShortName = '?' }); } [TestMethod] - public void TestHiddenArgument() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestHiddenArgument(ArgumentProviderKind kind) { - var parser = new CommandLineParser(); + var parser = CreateParser(kind); // Verify the hidden argument exists. - TestArgument(parser.GetArgument("Hidden"), new ExpectedArgument("Hidden", typeof(int)) { IsHidden = true }); + VerifyArgument(parser.GetArgument("Hidden"), new ExpectedArgument("Hidden", typeof(int)) { IsHidden = true }); // Verify it's not in the usage. var options = new UsageWriter() @@ -780,15 +758,16 @@ public void TestHiddenArgument() } [TestMethod] - public void TestNameTransformPascalCase() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformPascalCase(ArgumentProviderKind kind) { var options = new ParseOptions { ArgumentNameTransform = NameTransform.PascalCase }; - var parser = new CommandLineParser(options); - TestArguments(parser.Arguments, new[] + 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" }, @@ -800,15 +779,16 @@ public void TestNameTransformPascalCase() } [TestMethod] - public void TestNameTransformCamelCase() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformCamelCase(ArgumentProviderKind kind) { var options = new ParseOptions { ArgumentNameTransform = NameTransform.CamelCase }; - var parser = new CommandLineParser(options); - TestArguments(parser.Arguments, new[] + 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" }, @@ -820,15 +800,16 @@ public void TestNameTransformCamelCase() } [TestMethod] - public void TestNameTransformSnakeCase() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformSnakeCase(ArgumentProviderKind kind) { var options = new ParseOptions { ArgumentNameTransform = NameTransform.SnakeCase }; - var parser = new CommandLineParser(options); - TestArguments(parser.Arguments, new[] + 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" }, @@ -840,15 +821,16 @@ public void TestNameTransformSnakeCase() } [TestMethod] - public void TestNameTransformDashCase() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformDashCase(ArgumentProviderKind kind) { var options = new ParseOptions { ArgumentNameTransform = NameTransform.DashCase }; - var parser = new CommandLineParser(options); - TestArguments(parser.Arguments, new[] + 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" }, @@ -860,15 +842,16 @@ public void TestNameTransformDashCase() } [TestMethod] - public void TestValueDescriptionTransform() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestValueDescriptionTransform(ArgumentProviderKind kind) { var options = new ParseOptions { ValueDescriptionTransform = NameTransform.DashCase }; - var parser = new CommandLineParser(options); - TestArguments(parser.Arguments, new[] + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] { new ExpectedArgument("Arg1", typeof(FileInfo)) { ValueDescription = "file-info" }, new ExpectedArgument("Arg2", typeof(int)) { ValueDescription = "int32" }, @@ -938,9 +921,10 @@ public void TestValidation() } [TestMethod] - public void TestRequires() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestRequires(ArgumentProviderKind kind) { - var parser = new CommandLineParser(); + var parser = CreateParser(kind); var result = parser.Parse(new[] { "-Address", "127.0.0.1" }); Assert.AreEqual(IPAddress.Loopback, result.Address); @@ -958,9 +942,10 @@ public void TestRequires() } [TestMethod] - public void TestProhibits() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestProhibits(ArgumentProviderKind kind) { - var parser = new CommandLineParser(); + var parser = CreateParser(kind); var result = parser.Parse(new[] { "-Path", "test" }); Assert.AreEqual("test", result.Path.Name); @@ -968,9 +953,10 @@ public void TestProhibits() } [TestMethod] - public void TestRequiresAny() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestRequiresAny(ArgumentProviderKind kind) { - var parser = new CommandLineParser(); + var parser = CreateParser(kind); // No need to check if the arguments work indivially since TestRequires and TestProhibits already did that. CheckThrows(() => parser.Parse(Array.Empty()), parser, CommandLineArgumentErrorCategory.MissingRequiredArgument); @@ -995,7 +981,8 @@ public void TestValidatorUsageHelp() } [TestMethod] - public void TestDefaultValueDescriptions() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestDefaultValueDescriptions(ArgumentProviderKind kind) { var options = new ParseOptions() { @@ -1006,16 +993,17 @@ public void TestDefaultValueDescriptions() }, }; - var parser = new CommandLineParser(options); + 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); } [TestMethod] - public void TestMultiValueWhiteSpaceSeparator() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestMultiValueWhiteSpaceSeparator(ArgumentProviderKind kind) { - var parser = new CommandLineParser(); + var parser = CreateParser(kind); Assert.IsTrue(parser.GetArgument("Multi").AllowMultiValueWhiteSpaceSeparator); Assert.IsFalse(parser.GetArgument("MultiSwitch").AllowMultiValueWhiteSpaceSeparator); Assert.IsFalse(parser.GetArgument("Other").AllowMultiValueWhiteSpaceSeparator); @@ -1053,9 +1041,10 @@ public void TestMultiValueWhiteSpaceSeparator() //} [TestMethod] - public void TestDuplicateArguments() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestDuplicateArguments(ArgumentProviderKind kind) { - var parser = new CommandLineParser(); + var parser = CreateParser(kind); 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" }); @@ -1098,12 +1087,13 @@ public void TestDuplicateArguments() } [TestMethod] - public void TestConversion() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestConversion(ArgumentProviderKind kind) { - 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(' ')); + var parser = CreateParser(kind); + var result = parser.Parse("-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.Parse.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); @@ -1150,7 +1140,7 @@ public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind public bool IsHidden { get; set; } } - private static void TestArgument(CommandLineArgument argument, ExpectedArgument expected) + private static void VerifyArgument(CommandLineArgument argument, ExpectedArgument expected) { Assert.AreEqual(expected.Name, argument.ArgumentName); Assert.AreEqual(expected.MemberName ?? expected.Name, argument.MemberName); @@ -1175,13 +1165,13 @@ private static void TestArgument(CommandLineArgument argument, ExpectedArgument CollectionAssert.AreEqual(expected.ShortAliases, argument.ShortAliases); } - private static void TestArguments(IEnumerable arguments, ExpectedArgument[] expected) + private static void VerifyArguments(IEnumerable arguments, ExpectedArgument[] expected) { int index = 0; foreach (var arg in arguments) { Assert.IsTrue(index < expected.Length, "Too many arguments."); - TestArgument(arg, expected[index]); + VerifyArgument(arg, expected[index]); ++index; } } @@ -1228,7 +1218,7 @@ private static void TestParse(CommandLineParser target, string co if (arg15 == null) { - Assert.AreEqual(default(KeyValuePair), result.Arg15); + Assert.AreEqual(default, result.Arg15); } else { @@ -1261,5 +1251,43 @@ private static void CheckThrows(Action operation, CommandLineParser parser, Comm } } } + + private static CommandLineParser CreateParser(ArgumentProviderKind kind, ParseOptions options = null) + where T: class + { + var parser = kind switch + { + ArgumentProviderKind.Reflection => new CommandLineParser(options), + ArgumentProviderKind.Generated => (CommandLineParser)typeof(T).InvokeMember("CreateParser", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { options }), + _ => throw new InvalidOperationException() + }; + + Assert.AreEqual(kind, parser.ProviderKind); + return parser; + } + + private static T StaticParse(ArgumentProviderKind kind, string[] args, ParseOptions options = null) + where T : class + { + return kind switch + { + ArgumentProviderKind.Reflection => CommandLineParser.Parse(args, options), + ArgumentProviderKind.Generated => (T)typeof(T).InvokeMember("Parse", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { args, options }), + _ => throw new InvalidOperationException() + }; + } + + + public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data) + => $"{methodInfo.Name} ({data[0]})"; + + + public static IEnumerable ProviderKinds + => new[] + { + new object[] { ArgumentProviderKind.Reflection }, + new object[] { ArgumentProviderKind.Generated } + }; + } } diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index 18c95e9f..8247cc6b 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -86,7 +86,7 @@ class AsyncBaseCommand : AsyncCommandBase public override async Task RunAsync() { // Do something actually async to test the wait in Run(). - await Task.Delay(100); + await Task.Yield(); return 42; } } diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 4cd36a9b..4b03fa45 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1428,6 +1428,10 @@ internal void ApplyPropertyValue(object target) { throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex.InnerException, this); } + catch (Exception ex) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex, this); + } } internal void Reset() diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index e01b3333..e93d4044 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -721,6 +721,14 @@ public IEnumerable Validators /// 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 ArgumentProviderKind ProviderKind => _provider.Kind; + internal IComparer? ShortArgumentNameComparer => _argumentsByShortName?.Comparer; @@ -1440,8 +1448,21 @@ private void VerifyPositionalArgumentRules() // Run class validators. _provider.RunValidators(this); + object commandLineArguments; // TODO: Integrate with new ctor argument support. - object commandLineArguments = _provider.CreateInstance(this); + try + { + commandLineArguments = _provider.CreateInstance(this); + } + catch (TargetInvocationException ex) + { + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CreateArgumentsTypeError, ex.InnerException); + } + catch (Exception ex) + { + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CreateArgumentsTypeError, ex); + } + foreach (CommandLineArgument argument in _arguments) { // Apply property argument values (this does nothing for constructor or method arguments). diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs index 08c6738e..f0dabf9a 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -33,6 +33,14 @@ protected ArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, I _validators = validators ?? Enumerable.Empty(); } + /// + /// Gets the kind of argument provider. + /// + /// + /// One of the values of the enumeration. + /// + public virtual ArgumentProviderKind Kind => ArgumentProviderKind.Unknown; + /// /// Gets the type that will hold the argument values. /// diff --git a/src/Ookii.CommandLine/Support/ArgumentProviderKind.cs b/src/Ookii.CommandLine/Support/ArgumentProviderKind.cs new file mode 100644 index 00000000..55f9c739 --- /dev/null +++ b/src/Ookii.CommandLine/Support/ArgumentProviderKind.cs @@ -0,0 +1,20 @@ +namespace Ookii.CommandLine.Support; + +/// +/// Specifies the kind of provider that was the source of the arguments. +/// +public enum ArgumentProviderKind +{ + /// + /// A custom provider that was not part of Ookii.CommandLine. + /// + Unknown, + /// + /// An argument provider that uses reflection. + /// + Reflection, + /// + /// An argument provider that uses code generation. + /// + Generated +} diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 4f4658dd..156d2440 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -73,7 +73,6 @@ public static GeneratedArgument Create(CommandLineParser parser, multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); - // TODO: Set properly for multi-value and Nullable. info.ElementType = elementType; info.ElementTypeWithNullable = elementTypeWithNullable; info.Converter = converter; @@ -102,7 +101,14 @@ public static GeneratedArgument Create(CommandLineParser parser, throw new InvalidOperationException(); } - return _getProperty(target); + try + { + return _getProperty(target); + } + catch (Exception ex) + { + throw new TargetInvocationException(ex); + } } /// @@ -113,6 +119,13 @@ protected override void SetProperty(object target, object? value) throw new InvalidOperationException(); } - _setProperty(target, value); + try + { + _setProperty(target, value); + } + catch (Exception ex) + { + throw new TargetInvocationException(ex); + } } } diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs index 84662345..dedefe3c 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -10,10 +10,8 @@ namespace Ookii.CommandLine.Support; /// /// A base class for argument providers created by the . +/// This type is for internal use only and should not be used by your code. /// -/// -/// This type is for internal use and should not be used by your code. -/// public abstract class GeneratedArgumentProvider : ArgumentProvider { private readonly ApplicationFriendlyNameAttribute? _friendlyNameAttribute; @@ -45,6 +43,9 @@ protected GeneratedArgumentProvider(Type argumentsType, ParseOptionsAttribute? o _descriptionAttribute = description; } + /// + public override ArgumentProviderKind Kind => ArgumentProviderKind.Generated; + /// public override string ApplicationFriendlyName => _friendlyNameAttribute?.Name ?? ArgumentsType.Assembly.GetName().Name ?? string.Empty; diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs index a2521f8c..2d8bf64e 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -17,6 +17,8 @@ public ReflectionArgumentProvider(Type type) { } + public override ArgumentProviderKind Kind => ArgumentProviderKind.Reflection; + public override string ApplicationFriendlyName { get @@ -35,20 +37,13 @@ public override string ApplicationFriendlyName public override object CreateInstance(CommandLineParser parser) { var inject = ArgumentsType.GetConstructor(new[] { typeof(CommandLineParser) }) != null; - try + if (inject) { - if (inject) - { - return Activator.CreateInstance(ArgumentsType, parser)!; - } - else - { - return Activator.CreateInstance(ArgumentsType)!; - } + return Activator.CreateInstance(ArgumentsType, parser)!; } - catch (TargetInvocationException ex) + else { - throw parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.CreateArgumentsTypeError, ex.InnerException); + return Activator.CreateInstance(ArgumentsType)!; } } From b4ed799d4c1f42c44b19a20f63e7dfae732e67aa Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 11 Apr 2023 16:08:25 -0700 Subject: [PATCH 026/234] Method argument support. --- .../Diagnostics.cs | 11 ++ src/Ookii.CommandLine.Generator/Extensions.cs | 2 + .../ParserGenerator.cs | 120 +++++++++++++++++- .../Properties/Resources.Designer.cs | 18 +++ .../Properties/Resources.resx | 6 + src/Ookii.CommandLine.Generator/TypeHelper.cs | 22 ++++ src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 11 +- .../CommandLineParserTest.cs | 19 ++- src/Ookii.CommandLine/CommandLineArgument.cs | 13 +- .../Support/GeneratedArgument.cs | 39 +++--- src/Samples/TrimTest/Program.cs | 5 + 11 files changed, 224 insertions(+), 42 deletions(-) create mode 100644 src/Ookii.CommandLine.Generator/TypeHelper.cs diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index c54260a3..3205fa04 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -6,6 +6,7 @@ namespace Ookii.CommandLine.Generator; +// TODO: Help URIs. internal static class Diagnostics { private const string Category = "Ookii.CommandLine"; @@ -70,6 +71,16 @@ public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => isEnabledByDefault: true), member.Locations.FirstOrDefault(), elementType.ToDisplayString(), member.ContainingType?.ToDisplayString(), member.Name); + public static Diagnostic InvalidMethodSignature(ISymbol method) => Diagnostic.Create( + new DiagnosticDescriptor( + "CL1007", + new LocalizableResourceString(nameof(Resources.InvalidMethodSignatureTitle), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.InvalidMethodSignatureMessageFormat), Resources.ResourceManager, typeof(Resources)), + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true), + method.Locations.FirstOrDefault(), method.ContainingType?.ToDisplayString(), method.Name); + public static Diagnostic UnknownAttribute(AttributeData attribute) => Diagnostic.Create( new DiagnosticDescriptor( "CLW1001", diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index a889dd67..4df0623f 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -137,4 +137,6 @@ public static string ToFullCSharpString(this TypedConstant constant) _ => constant.ToCSharpString(), }; } + + public static bool DefaultEquals(this ISymbol left, ISymbol? right) => SymbolEqualityComparer.Default.Equals(left, right); } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 058754c0..d164f749 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -12,6 +12,15 @@ namespace Ookii.CommandLine.Generator; internal class ParserGenerator { + private struct MethodArgumentInfo + { + public ITypeSymbol ArgumentType { get; set; } + public bool HasValueParameter { get; set; } + public bool HasParserParameter { get; set; } + public bool HasBooleanReturn { get; set; } + } + + private readonly TypeHelper _typeHelper; private readonly Compilation _compilation; private readonly SourceProductionContext _context; private readonly INamedTypeSymbol _argumentsClass; @@ -20,6 +29,7 @@ internal class ParserGenerator public ParserGenerator(Compilation compilation, SourceProductionContext context, INamedTypeSymbol argumentsClass, ConverterGenerator converterGenerator) { + _typeHelper = new TypeHelper(compilation); _compilation = compilation; _context = context; _argumentsClass = argumentsClass; @@ -164,19 +174,36 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) return; } + ITypeSymbol originalArgumentType; + MethodArgumentInfo? methodInfo = null; var property = member as IPropertySymbol; - var method = member as IMethodSymbol; - if (method != null) + if (property != null) { - throw new NotImplementedException(); + if (property.IsStatic) + { + // TODO: Warning or error? + return; + } + + originalArgumentType = property.Type; } + else if (member is IMethodSymbol method) + { + methodInfo = DetermineMethodArgumentInfo(method); + if (methodInfo is not MethodArgumentInfo methodInfoValue) + { + _context.ReportDiagnostic(Diagnostics.InvalidMethodSignature(method)); + return; + } - if (property == null || property.IsStatic) + originalArgumentType = methodInfoValue.ArgumentType; + } + else { + // How did we get here? Already checked above. return; } - var originalArgumentType = property!.Type; var argumentType = originalArgumentType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); var notNullAnnotation = string.Empty; var allowsNull = originalArgumentType.AllowsNull(); @@ -326,12 +353,41 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) if (property?.SetMethod?.DeclaredAccessibility == Accessibility.Public) { - _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.Name})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"); + _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.ToDisplayString()})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"); } if (property != null) { - _builder.AppendLine($", getProperty: (target) => (({_argumentsClass.Name})target).{member.Name}"); + _builder.AppendLine($", getProperty: (target) => (({_argumentsClass.ToDisplayString()})target).{member.Name}"); + } + + if (methodInfo is MethodArgumentInfo info) + { + string arguments = string.Empty; + if (info.HasValueParameter) + { + if (info.HasParserParameter) + { + arguments = $"({originalArgumentType.ToDisplayString()})value{notNullAnnotation}, parser"; + } + else + { + arguments = $"({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"; + } + } + else if (info.HasParserParameter) + { + arguments = "parser"; + } + + if (info.HasBooleanReturn) + { + _builder.AppendLine($", callMethod: (value, parser) => {_argumentsClass.ToDisplayString()}.{member.Name}({arguments})"); + } + else + { + _builder.AppendLine($", callMethod: (value, parser) => {{ {_argumentsClass.ToDisplayString()}.{member.Name}({arguments}); return true; }}"); + } } _builder.DecreaseIndent(); @@ -475,4 +531,54 @@ private static bool CheckAttribute(AttributeData data, string name, ref List 2) + { + return null; + } + + var info = new MethodArgumentInfo(); + if (method.ReturnType.DefaultEquals(_typeHelper.Boolean)) + { + info.HasBooleanReturn = true; + } + else if (!method.ReturnType.DefaultEquals(_typeHelper.Void)) + { + return null; + } + + if (parameters.Length == 2) + { + info.ArgumentType = parameters[0].Type; + if (!parameters[1].Type.DefaultEquals(_typeHelper.CommandLineParser)) + { + return null; + } + + info.HasValueParameter = true; + info.HasParserParameter = true; + } + else if (parameters.Length == 1) + { + if (parameters[0].Type.DefaultEquals(_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; + } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 373262e7..1253f191 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -132,6 +132,24 @@ internal static string InvalidArrayRankTitle { } } + /// + /// Looks up a localized string similar to The method {0}.{1} does not have a valid signature for a method argument.. + /// + internal static string InvalidMethodSignatureMessageFormat { + get { + return ResourceManager.GetString("InvalidMethodSignatureMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A method argument has an invalid signature.. + /// + internal static string InvalidMethodSignatureTitle { + get { + return ResourceManager.GetString("InvalidMethodSignatureTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to No argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 0b094837..6aa5e6ba 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -141,6 +141,12 @@ A multi-value argument defined by an array properties must have an array rank of one. + + The method {0}.{1} does not have a valid signature for a method argument. + + + A method argument has an invalid signature. + No argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter. diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs new file mode 100644 index 00000000..e017210e --- /dev/null +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -0,0 +1,22 @@ +using Microsoft.CodeAnalysis; + +namespace Ookii.CommandLine.Generator; + +internal class TypeHelper +{ + private readonly Compilation _compilation; + private const string NamespacePrefix = "Ookii.CommandLine."; + + public TypeHelper(Compilation compilation) + { + _compilation = compilation; + } + + public INamedTypeSymbol? String => _compilation.GetTypeByMetadataName("System.String"); + + public INamedTypeSymbol? Void => _compilation.GetTypeByMetadataName("System.Void"); + + public INamedTypeSymbol? Boolean => _compilation.GetTypeByMetadataName("System.Boolean"); + + public INamedTypeSymbol? CommandLineParser => _compilation.GetTypeByMetadataName(NamespacePrefix + "CommandLineParser"); +} diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 11641679..85388e5e 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -238,7 +238,8 @@ partial class LongShortArguments public int Bar { get; set; } } - class MethodArguments + [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. @@ -298,11 +299,6 @@ public static void Positional(int value) Value = value; } - [CommandLineArgument] - public void NotStatic() - { - } - [CommandLineArgument] private static void NotPublic() { @@ -367,7 +363,8 @@ partial class ValueDescriptionTransformArguments public int Arg2 { get; set; } } - class ValidationArguments + [GeneratedParser] + partial class ValidationArguments { public static int Arg3Value { get; set; } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 22dd5a11..3c8c4d19 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -674,9 +674,10 @@ public void TestLongShortMode(ArgumentProviderKind kind) } [TestMethod] - public void TestMethodArguments() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestMethodArguments(ArgumentProviderKind kind) { - var parser = new CommandLineParser(); + var parser = CreateParser(kind); Assert.AreEqual(ArgumentKind.Method, parser.GetArgument("NoCancel").Kind); Assert.IsNull(parser.GetArgument("NotAnArgument")); @@ -861,9 +862,12 @@ public void TestValueDescriptionTransform(ArgumentProviderKind kind) } [TestMethod] - public void TestValidation() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestValidation(ArgumentProviderKind kind) { - var parser = new CommandLineParser(); + // Reset for multiple runs. + ValidationArguments.Arg3Value = 0; + var parser = CreateParser(kind); // Range validator on property CheckThrows(() => parser.Parse(new[] { "-Arg1", "0" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg1"); @@ -963,9 +967,10 @@ public void TestRequiresAny(ArgumentProviderKind kind) } [TestMethod] - public void TestValidatorUsageHelp() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestValidatorUsageHelp(ArgumentProviderKind kind) { - CommandLineParser parser = new CommandLineParser(); + CommandLineParser parser = CreateParser(kind); var options = new UsageWriter() { ExecutableName = _executableName, @@ -973,7 +978,7 @@ public void TestValidatorUsageHelp() Assert.AreEqual(_expectedUsageValidators, parser.GetUsage(options)); - parser = new CommandLineParser(); + parser = CreateParser(kind); Assert.AreEqual(_expectedUsageDependencies, parser.GetUsage(options)); options.IncludeValidatorsInDescription = false; diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 4b03fa45..241da7b4 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -159,7 +159,18 @@ public void ApplyValue(CommandLineArgument argument, object target) public bool SetValue(CommandLineArgument argument, object? value) { Value = value; - return argument.CallMethod(value); + try + { + return argument.CallMethod(value); + } + catch (TargetInvocationException ex) + { + throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex.InnerException, argument, value?.ToString()); + } + catch (Exception ex) + { + throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ApplyValueError, ex, argument, value?.ToString()); + } } } diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 156d2440..81c24a6e 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -17,11 +17,14 @@ public class GeneratedArgument : CommandLineArgument { private readonly Action? _setProperty; private readonly Func? _getProperty; + private readonly Func? _callMethod; - private GeneratedArgument(ArgumentInfo info, Action? setProperty, Func? getProperty) : base(info) + private GeneratedArgument(ArgumentInfo info, Action? setProperty, Func? getProperty, + Func? callMethod) : base(info) { _setProperty = setProperty; _getProperty = getProperty; + _callMethod = callMethod; } /// @@ -47,6 +50,7 @@ private GeneratedArgument(ArgumentInfo info, Action? setPropert /// /// /// + /// /// public static GeneratedArgument Create(CommandLineParser parser, Type argumentType, @@ -67,7 +71,8 @@ public static GeneratedArgument Create(CommandLineParser parser, IEnumerable? shortAliasAttributes = null, IEnumerable? validationAttributes = null, Action? setProperty = null, - Func? getProperty = null) + Func? getProperty = null, + Func? callMethod = null) { var info = CreateArgumentInfo(parser, argumentType, allowsNull, memberName, attribute, multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, @@ -84,14 +89,22 @@ public static GeneratedArgument Create(CommandLineParser parser, info.ValueType = valueType; } - return new GeneratedArgument(info, setProperty, getProperty); + return new GeneratedArgument(info, setProperty, getProperty, callMethod); } /// protected override bool CanSetProperty => _setProperty != null; /// - protected override bool CallMethod(object? value) => throw new NotImplementedException(); + protected override bool CallMethod(object? value) + { + if (_callMethod == null) + { + throw new InvalidOperationException(); + } + + return _callMethod(value, this.Parser); + } /// protected override object? GetProperty(object target) @@ -101,14 +114,7 @@ public static GeneratedArgument Create(CommandLineParser parser, throw new InvalidOperationException(); } - try - { - return _getProperty(target); - } - catch (Exception ex) - { - throw new TargetInvocationException(ex); - } + return _getProperty(target); } /// @@ -119,13 +125,6 @@ protected override void SetProperty(object target, object? value) throw new InvalidOperationException(); } - try - { - _setProperty(target, value); - } - catch (Exception ex) - { - throw new TargetInvocationException(ex); - } + _setProperty(target, value); } } diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index c2ac2adf..4e5f2d1a 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -45,4 +45,9 @@ partial class Arguments [CommandLineArgument] public IDictionary Arg14 { get; } = new SortedDictionary(); + + [CommandLineArgument] + public static void Foo(CommandLineParser p) + { + } } From 4b61ec57c7c8a423fe2af34a913e2dff20bc8d25 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 11 Apr 2023 16:28:54 -0700 Subject: [PATCH 027/234] Run nullable tests with generated parser. --- .../ConverterGenerator.cs | 9 +- .../Diagnostics.cs | 10 + src/Ookii.CommandLine.Generator/Extensions.cs | 12 + .../ParserGenerator.cs | 4 +- .../ParserIncrementalGenerator.cs | 9 +- .../Properties/Resources.Designer.cs | 18 + .../Properties/Resources.resx | 6 + .../CommandLineParserNullableTest.cs | 363 +++++++++--------- .../CommandLineParserTest.cs | 3 +- 9 files changed, 247 insertions(+), 187 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 04435eeb..1e95c16e 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -178,14 +178,7 @@ public ConverterGenerator(Compilation compilation) private static string GenerateName(string displayName) { - var builder = new StringBuilder(displayName.Length + ConverterSuffix.Length); - foreach (var ch in displayName) - { - builder.Append(char.IsLetterOrDigit(ch) ? ch : '_'); - } - - builder.Append(ConverterSuffix); - return builder.ToString(); + return displayName.ToIdentifier(ConverterSuffix); } private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, ConverterInfo info) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 3205fa04..6ada7924 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -81,6 +81,16 @@ public static Diagnostic InvalidMethodSignature(ISymbol method) => Diagnostic.Cr isEnabledByDefault: true), method.Locations.FirstOrDefault(), method.ContainingType?.ToDisplayString(), method.Name); + public static Diagnostic ArgumentsClassIsNested(INamedTypeSymbol symbol) => Diagnostic.Create( + new DiagnosticDescriptor( + "CL1008", + new LocalizableResourceString(nameof(Resources.ArgumentsClassIsNestedTitle), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.ArgumentsClassIsNestedMessageFormat), Resources.ResourceManager, typeof(Resources)), + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true), + symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic UnknownAttribute(AttributeData attribute) => Diagnostic.Create( new DiagnosticDescriptor( "CLW1001", diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 4df0623f..f59e7362 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -139,4 +139,16 @@ public static string ToFullCSharpString(this TypedConstant constant) } public static bool DefaultEquals(this ISymbol left, ISymbol? right) => SymbolEqualityComparer.Default.Equals(left, right); + + public static string ToIdentifier(this string displayName, string suffix) + { + var builder = new StringBuilder(displayName.Length + suffix.Length); + foreach (var ch in displayName) + { + builder.Append(char.IsLetterOrDigit(ch) ? ch : '_'); + } + + builder.Append(suffix); + return builder.ToString(); + } } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index d164f749..45661ac7 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -274,9 +274,9 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) { Debug.Assert(multiValueElementType != null); kind = "Ookii.CommandLine.ArgumentKind.MultiValue"; - elementTypeWithNullable = multiValueElementType!; + allowsNull = multiValueElementType!.AllowsNull(); + elementTypeWithNullable = multiValueElementType!.WithNullableAnnotation(NullableAnnotation.NotAnnotated); namedElementTypeWithNullable = elementTypeWithNullable as INamedTypeSymbol; - allowsNull = elementTypeWithNullable.AllowsNull(); } } else diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index d2d5ba86..de392279 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -60,11 +60,16 @@ private static void Execute(Compilation compilation, ImmutableArray + /// Looks up a localized string similar to The arguments class {0} may not be nested in another type when the GeneratedParserAttribute is used.. + /// + internal static string ArgumentsClassIsNestedMessageFormat { + get { + return ResourceManager.GetString("ArgumentsClassIsNestedMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The arguments class may not be a nested type.. + /// + internal static string ArgumentsClassIsNestedTitle { + get { + return ResourceManager.GetString("ArgumentsClassIsNestedTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to The arguments class {0} must use the 'partial' modifier.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 6aa5e6ba..c6402bd1 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -123,6 +123,12 @@ The arguments class may not be a generic type. + + The arguments class {0} may not be nested in another type when the GeneratedParserAttribute is used. + + + The arguments class may not be a nested type. + The arguments class {0} must use the 'partial' modifier. diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs index 9b503848..dd618ef5 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -6,180 +6,22 @@ #nullable enable using Microsoft.VisualStudio.TestTools.UnitTesting; using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Support; using System; using System.Collections.Generic; using System.Globalization; +using System.Reflection; namespace Ookii.CommandLine.Tests { [TestClass] public class CommandLineParserNullableTest { -#region Nested types - - 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); - } - } - } - - class TestArguments - { - // TODO: Put back with new ctor approach. - //public TestArguments( - // [ArgumentConverter(typeof(NullReturningStringConverter))] string? constructorNullable, - // [ArgumentConverter(typeof(NullReturningStringConverter))] string constructorNonNullable, - // [ArgumentConverter(typeof(NullReturningIntConverter))] int constructorValueType, - // [ArgumentConverter(typeof(NullReturningIntConverter))] int? constructorNullableValueType) - //{ - // ConstructorNullable = constructorNullable; - // ConstructorNonNullable = constructorNonNullable; - // ConstructorValueType = constructorValueType; - // ConstructorNullableValueType = constructorNullableValueType; - //} - - [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; } - } - -#endregion - [TestMethod] - public void TestAllowNull() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestAllowNull(ArgumentProviderKind kind) { - var parser = new CommandLineParser(typeof(TestArguments)); + var parser = CommandLineParserTest.CreateParser(kind); Assert.IsTrue(parser.GetArgument("constructorNullable")!.AllowNull); Assert.IsFalse(parser.GetArgument("constructorNonNullable")!.AllowNull); Assert.IsFalse(parser.GetArgument("constructorValueType")!.AllowNull); @@ -210,9 +52,11 @@ public void TestAllowNull() } [TestMethod] - public void TestNonNullableConstructor() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNonNullableConstructor(ArgumentProviderKind kind) { - var parser = new CommandLineParser(typeof(TestArguments)); + // TODO: Update for new ctor arguments style. + var parser = CommandLineParserTest.CreateParser(kind); ExpectNullException(parser, "constructorNonNullable", "foo", "(null)", "4", "5"); ExpectNullException(parser, "constructorValueType", "foo", "bar", "(null)", "5"); var result = ExpectSuccess(parser, "(null)", "bar", "4", "(null)"); @@ -223,9 +67,10 @@ public void TestNonNullableConstructor() } [TestMethod] - public void TestNonNullableProperties() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNonNullableProperties(ArgumentProviderKind kind) { - var parser = new CommandLineParser(typeof(TestArguments)); + 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)"); @@ -236,9 +81,10 @@ public void TestNonNullableProperties() } [TestMethod] - public void TestNonNullableMultiValue() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNonNullableMultiValue(ArgumentProviderKind kind) { - var parser = new CommandLineParser(typeof(TestArguments)); + 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)"); @@ -264,9 +110,10 @@ public void TestNonNullableMultiValue() } [TestMethod] - public void TestNonNullableDictionary() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNonNullableDictionary(ArgumentProviderKind kind) { - var parser = new CommandLineParser(typeof(TestArguments)); + 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)"); @@ -310,13 +157,183 @@ private static void ExpectNullException(CommandLineParser parser, string argumen } } - private static TestArguments ExpectSuccess(CommandLineParser parser, params string[] args) + private static NullableArguments ExpectSuccess(CommandLineParser parser, params string[] args) { - var result = (TestArguments?)parser.Parse(args); + var result = (NullableArguments?)parser.Parse(args); Assert.IsNotNull(result); return result; } + + public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data) + => $"{methodInfo.Name} ({data[0]})"; + + + public static IEnumerable ProviderKinds + => new[] + { + new object[] { ArgumentProviderKind.Reflection }, + new object[] { ArgumentProviderKind.Generated } + }; } + + 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 + { + // TODO: Put back with new ctor approach. + //public TestArguments( + // [ArgumentConverter(typeof(NullReturningStringConverter))] string? constructorNullable, + // [ArgumentConverter(typeof(NullReturningStringConverter))] string constructorNonNullable, + // [ArgumentConverter(typeof(NullReturningIntConverter))] int constructorValueType, + // [ArgumentConverter(typeof(NullReturningIntConverter))] int? constructorNullableValueType) + //{ + // ConstructorNullable = constructorNullable; + // ConstructorNonNullable = constructorNonNullable; + // ConstructorValueType = constructorValueType; + // ConstructorNullableValueType = constructorNullableValueType; + //} + + [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 diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 3c8c4d19..9cb59614 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1257,7 +1257,7 @@ private static void CheckThrows(Action operation, CommandLineParser parser, Comm } } - private static CommandLineParser CreateParser(ArgumentProviderKind kind, ParseOptions options = null) + internal static CommandLineParser CreateParser(ArgumentProviderKind kind, ParseOptions options = null) where T: class { var parser = kind switch @@ -1293,6 +1293,5 @@ public static IEnumerable ProviderKinds new object[] { ArgumentProviderKind.Reflection }, new object[] { ArgumentProviderKind.Generated } }; - } } From 69b9d1f18f6d66b59cc289c478f1c36cd0438fad Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 11 Apr 2023 17:55:36 -0700 Subject: [PATCH 028/234] Static interfaces for .Net 7. --- .../ParserGenerator.cs | 8 ++ src/Ookii.CommandLine.Generator/TypeHelper.cs | 8 +- .../CommandLineParserTest.cs | 18 ++++- .../Ookii.CommandLine.Tests.csproj | 2 +- src/Ookii.CommandLine/CommandLineParser.cs | 3 - src/Ookii.CommandLine/IParser.cs | 78 +++++++++++++++++++ src/Ookii.CommandLine/IParserProvider.cs | 47 +++++++++++ 7 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 src/Ookii.CommandLine/IParser.cs create mode 100644 src/Ookii.CommandLine/IParserProvider.cs diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 45661ac7..3698acf1 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -46,11 +46,19 @@ public ParserGenerator(Compilation compilation, SourceProductionContext context, public string? Generate() { _builder.AppendLine($"partial class {_argumentsClass.Name}"); + if (_typeHelper.IParser != null) + { + _builder.AppendLine($" : Ookii.CommandLine.IParser<{_argumentsClass.Name}>"); + } + _builder.OpenBlock(); GenerateProvider(); _builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{_argumentsClass.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new GeneratedProvider(), options);"); _builder.AppendLine(); var nullableType = _argumentsClass.WithNullableAnnotation(NullableAnnotation.Annotated); + // TODO: Optionally implement these. + // We cannot rely on default implementations, because that makes the methods uncallable + // without a generic type argument. _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling();"); _builder.AppendLine(); _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index e017210e..921760b7 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -12,11 +12,13 @@ public TypeHelper(Compilation compilation) _compilation = compilation; } - public INamedTypeSymbol? String => _compilation.GetTypeByMetadataName("System.String"); + public INamedTypeSymbol? String => _compilation.GetSpecialType(SpecialType.System_String); - public INamedTypeSymbol? Void => _compilation.GetTypeByMetadataName("System.Void"); + public INamedTypeSymbol? Void => _compilation.GetSpecialType(SpecialType.System_Void); - public INamedTypeSymbol? Boolean => _compilation.GetTypeByMetadataName("System.Boolean"); + public INamedTypeSymbol? Boolean => _compilation.GetSpecialType(SpecialType.System_Boolean); public INamedTypeSymbol? CommandLineParser => _compilation.GetTypeByMetadataName(NamespacePrefix + "CommandLineParser"); + + public INamedTypeSymbol? IParser => _compilation.GetTypeByMetadataName(NamespacePrefix + "IParserProvider`1"); } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 9cb59614..f53b1a3d 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1258,12 +1258,20 @@ private static void CheckThrows(Action operation, CommandLineParser parser, Comm } internal static CommandLineParser CreateParser(ArgumentProviderKind kind, ParseOptions options = null) - where T: class +#if NET7_0_OR_GREATER + where T : class, IParserProvider +#else + where T : class +#endif { var parser = kind switch { ArgumentProviderKind.Reflection => new CommandLineParser(options), +#if NET7_0_OR_GREATER + ArgumentProviderKind.Generated => T.CreateParser(options), +#else ArgumentProviderKind.Generated => (CommandLineParser)typeof(T).InvokeMember("CreateParser", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { options }), +#endif _ => throw new InvalidOperationException() }; @@ -1272,12 +1280,20 @@ internal static CommandLineParser CreateParser(ArgumentProviderKind kind, } private static T StaticParse(ArgumentProviderKind kind, string[] args, ParseOptions options = null) +#if NET7_0_OR_GREATER + where T : class, IParser +#else where T : class +#endif { return kind switch { ArgumentProviderKind.Reflection => CommandLineParser.Parse(args, options), +#if NET7_0_OR_GREATER + ArgumentProviderKind.Generated => T.Parse(args, options), +#else ArgumentProviderKind.Generated => (T)typeof(T).InvokeMember("Parse", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { args, options }), +#endif _ => throw new InvalidOperationException() }; } diff --git a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj index 0a0c84f9..d37e1888 100644 --- a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj +++ b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj @@ -5,7 +5,7 @@ disable Tests for Ookii.CommandLine. false - 9.0 + 11.0 true diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index e93d4044..d401050f 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -996,9 +996,6 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// error occurred, or argument parsing was canceled by the /// property or a method argument that returned . /// - /// - /// - /// /// /// /// This is a convenience function that instantiates a , diff --git a/src/Ookii.CommandLine/IParser.cs b/src/Ookii.CommandLine/IParser.cs new file mode 100644 index 00000000..e2c6c246 --- /dev/null +++ b/src/Ookii.CommandLine/IParser.cs @@ -0,0 +1,78 @@ +#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 (on .Net 7 and later only) when the +/// is used. Classes without that attribute must parse +/// arguments using the +/// method, or create the parser directly by using the +/// constructor directly; these classes do not support this interface unless it is manually +/// implemented. +/// +/// +public interface IParser : IParserProvider + where TSelf : class, IParser +{ + /// + /// Parses the arguments returned by the + /// method using the type . + /// + /// + /// 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 . + /// + /// + public static abstract TSelf? Parse(ParseOptions? options = null); + + /// + /// Parses the specified command line arguments using the type . + /// + /// 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 . + /// + /// + public static abstract TSelf? Parse(string[] args, ParseOptions? options = null); + + /// + /// Parses the specified command line arguments, starting at the specified index, using the + /// type . + /// + /// 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. + /// + /// + /// An instance of the type , or if an + /// error occurred, or argument parsing was canceled by the + /// property or a method argument that returned . + /// + /// + public static abstract TSelf? Parse(string[] args, int index, ParseOptions? options = null); +} + +#endif diff --git a/src/Ookii.CommandLine/IParserProvider.cs b/src/Ookii.CommandLine/IParserProvider.cs new file mode 100644 index 00000000..502fad45 --- /dev/null +++ b/src/Ookii.CommandLine/IParserProvider.cs @@ -0,0 +1,47 @@ +#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 (on .Net 7 and later only) when the +/// is used. Classes without that attribute must create +/// the parser directly by using the +/// constructor directly; these classes do not support this interface unless it is manually +/// implemented. +/// +/// +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. + /// + /// + public static abstract CommandLineParser CreateParser(ParseOptions? options = null); +} + +#endif From 76d8714b90daaa289c884b6d8c01119310701480 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 11 Apr 2023 18:24:56 -0700 Subject: [PATCH 029/234] Better type checking. --- src/Ookii.CommandLine.Generator/Extensions.cs | 40 ++++++++++--------- .../ParserGenerator.cs | 27 ++++++------- src/Ookii.CommandLine.Generator/TypeHelper.cs | 12 ++++-- .../GeneratedParserAttribute.cs | 2 +- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index f59e7362..94268161 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -42,7 +42,7 @@ public static bool DerivesFrom(this ITypeSymbol type, ITypeSymbol baseClass) } public static bool IsNullableValueType(this INamedTypeSymbol type) - => !type.IsReferenceType && type.IsGenericType && type.ConstructedFrom.ToDisplayString() == "System.Nullable"; + => !type.IsReferenceType && type.IsGenericType && type.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; public static bool IsNullableValueType(this ITypeSymbol type) => type is INamedTypeSymbol namedType && namedType.IsNullableValueType(); @@ -56,18 +56,23 @@ public static INamedTypeSymbol GetUnderlyingType(this INamedTypeSymbol type) public static ITypeSymbol GetUnderlyingType(this ITypeSymbol type) => type is INamedTypeSymbol namedType && namedType.IsNullableValueType() ? (INamedTypeSymbol)namedType.TypeArguments[0] : type; - public static bool IsEnum(this ITypeSymbol type) => type.BaseType?.ToDisplayString() == "System.Enum"; + public static bool IsEnum(this ITypeSymbol type) => type.BaseType?.SpecialType == SpecialType.System_Enum; - public static INamedTypeSymbol? FindGenericInterface(this ITypeSymbol type, string interfaceName) + public static INamedTypeSymbol? FindGenericInterface(this ITypeSymbol type, ITypeSymbol? interfaceToFind) { - if (type.TypeKind == TypeKind.Interface && ((INamedTypeSymbol)type).IsTypeOrConstructedFrom(interfaceName)) + if (interfaceToFind == null) + { + return null; + } + + if (type.TypeKind == TypeKind.Interface && ((INamedTypeSymbol)type).IsConstructedFrom(interfaceToFind)) { return (INamedTypeSymbol)type; } foreach (var iface in type.AllInterfaces) { - if (iface.IsTypeOrConstructedFrom(interfaceName)) + if (iface.IsConstructedFrom(interfaceToFind)) { return iface; } @@ -76,7 +81,7 @@ public static ITypeSymbol GetUnderlyingType(this ITypeSymbol type) return null; } - public static bool IsTypeOrConstructedFrom(this INamedTypeSymbol type, string name) + public static bool IsConstructedFrom(this INamedTypeSymbol type, ITypeSymbol typeDefinition) { var realType = type; if (realType.IsGenericType) @@ -84,27 +89,24 @@ public static bool IsTypeOrConstructedFrom(this INamedTypeSymbol type, string na realType = realType.ConstructedFrom; } - return realType.ToDisplayString() == name; + return realType.SymbolEquals(typeDefinition); } - public static bool ImplementsInterface(this ITypeSymbol symbol, string interfaceName) + public static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol? interfaceType) { - foreach (var iface in symbol.AllInterfaces) + if (interfaceType == null) { - if (iface.ToDisplayString() == interfaceName) - { - return true; - } + return false; } - return false; - } + if (type.SymbolEquals(interfaceType)) + { + return true; + } - public static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol interfaceType) - { foreach (var iface in type.AllInterfaces) { - if (SymbolEqualityComparer.Default.Equals(iface, interfaceType)) + if (iface.SymbolEquals(interfaceType)) { return true; } @@ -138,7 +140,7 @@ public static string ToFullCSharpString(this TypedConstant constant) }; } - public static bool DefaultEquals(this ISymbol left, ISymbol? right) => SymbolEqualityComparer.Default.Equals(left, right); + public static bool SymbolEquals(this ISymbol left, ISymbol? right) => SymbolEqualityComparer.Default.Equals(left, right); public static string ToIdentifier(this string displayName, string suffix) { diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 3698acf1..206ae171 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -433,7 +433,7 @@ private static bool CheckAttribute(AttributeData data, string name, ref List it doesn't matter if the property is // read-only or not. - if (namedType.IsGenericType && namedType.ConstructedFrom.ToDisplayString() == "System.Collections.Generic.Dictionary") + if (namedType.IsGenericType && namedType.ConstructedFrom.SymbolEquals(_typeHelper.Dictionary)) { var keyValuePair = _compilation.GetTypeByMetadataName(typeof(KeyValuePair<,>).FullName)!; var elementType = keyValuePair.Construct(namedType.TypeArguments, namedType.TypeArgumentNullableAnnotations); @@ -466,7 +466,7 @@ private static bool CheckAttribute(AttributeData data, string name, ref List"); + var dictionaryType = argumentType.FindGenericInterface(_typeHelper.IDictionary); if (dictionaryType != null) { var keyValuePair = _compilation.GetTypeByMetadataName(typeof(KeyValuePair<,>).FullName)!; @@ -474,7 +474,7 @@ private static bool CheckAttribute(AttributeData data, string name, ref List"); + var collectionType = argumentType.FindGenericInterface(_typeHelper.ICollection); if (collectionType != null) { var elementType = collectionType.TypeArguments[0]; @@ -512,13 +512,12 @@ private static bool CheckAttribute(AttributeData data, string name, ref List")) + if (elementType.ImplementsInterface(_typeHelper.ISpanParsable?.Construct(elementType))) { return $"new Ookii.CommandLine.Conversion.SpanParsableConverter<{elementType.ToDisplayString()}>()"; } - if (elementType.ImplementsInterface($"System.IParsable<{elementType.ToDisplayString()}>")) + if (elementType.ImplementsInterface(_typeHelper.IParsable?.Construct(elementType))) { return $"new Ookii.CommandLine.Conversion.ParsableConverter<{elementType.ToDisplayString()}>()"; } @@ -549,11 +548,11 @@ private static bool CheckAttribute(AttributeData data, string name, ref List _compilation.GetSpecialType(SpecialType.System_String); + public INamedTypeSymbol? Boolean => _compilation.GetSpecialType(SpecialType.System_Boolean); - public INamedTypeSymbol? Void => _compilation.GetSpecialType(SpecialType.System_Void); + public INamedTypeSymbol? Dictionary => _compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName); - public INamedTypeSymbol? Boolean => _compilation.GetSpecialType(SpecialType.System_Boolean); + public INamedTypeSymbol? IDictionary => _compilation.GetTypeByMetadataName(typeof(IDictionary<,>).FullName); + + public INamedTypeSymbol? ICollection => _compilation.GetTypeByMetadataName(typeof(ICollection<>).FullName); + + public INamedTypeSymbol? ISpanParsable => _compilation.GetTypeByMetadataName("System.ISpanParsable`1"); + + public INamedTypeSymbol? IParsable => _compilation.GetTypeByMetadataName("System.IParsable`1"); public INamedTypeSymbol? CommandLineParser => _compilation.GetTypeByMetadataName(NamespacePrefix + "CommandLineParser"); diff --git a/src/Ookii.CommandLine/GeneratedParserAttribute.cs b/src/Ookii.CommandLine/GeneratedParserAttribute.cs index ec8e2379..2c21515d 100644 --- a/src/Ookii.CommandLine/GeneratedParserAttribute.cs +++ b/src/Ookii.CommandLine/GeneratedParserAttribute.cs @@ -7,6 +7,6 @@ namespace Ookii.CommandLine; /// TODO: Better help. /// [AttributeUsage(AttributeTargets.Class)] -public class GeneratedParserAttribute : Attribute +public sealed class GeneratedParserAttribute : Attribute { } From ea5fb5b23f4ed8fdf22aa5a0c52a60f707a8e3ed Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 11 Apr 2023 20:58:24 -0700 Subject: [PATCH 030/234] Improved attribute type checking. --- .../AttributeNames.cs | 27 ------------ src/Ookii.CommandLine.Generator/Extensions.cs | 21 +++------- .../ParserGenerator.cs | 42 +++++++++---------- .../ParserIncrementalGenerator.cs | 4 +- src/Ookii.CommandLine.Generator/TypeHelper.cs | 33 +++++++++++++++ 5 files changed, 61 insertions(+), 66 deletions(-) delete mode 100644 src/Ookii.CommandLine.Generator/AttributeNames.cs diff --git a/src/Ookii.CommandLine.Generator/AttributeNames.cs b/src/Ookii.CommandLine.Generator/AttributeNames.cs deleted file mode 100644 index 50f1b7c1..00000000 --- a/src/Ookii.CommandLine.Generator/AttributeNames.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Ookii.CommandLine.Generator; - -internal static class AttributeNames -{ - public const string NamespacePrefix = "Ookii.CommandLine."; - public const string GeneratedParser = NamespacePrefix + "GeneratedParserAttribute"; - public const string CommandLineArgument = NamespacePrefix + "CommandLineArgumentAttribute"; - public const string ParseOptions = NamespacePrefix + "ParseOptionsAttribute"; - public const string ApplicationFriendlyName = NamespacePrefix + "ApplicationFriendlyNameAttribute"; - public const string Command = NamespacePrefix + "Commands.CommandAttribute"; - public const string ClassValidation = NamespacePrefix + "Validation.ClassValidationAttribute"; - public const string MultiValueSeparator = NamespacePrefix + "MultiValueSeparatorAttribute"; - public const string KeyValueSeparator = NamespacePrefix + "Conversion.KeyValueSeparatorAttribute"; - public const string AllowDuplicateDictionaryKeys = NamespacePrefix + "AllowDuplicateDictionaryKeysAttribute"; - public const string Alias = NamespacePrefix + "AliasAttribute"; - public const string ShortAlias = NamespacePrefix + "ShortAliasAttribute"; - public const string ArgumentValidation = NamespacePrefix + "Validation.ArgumentValidationAttribute"; - public const string ArgumentConverter = NamespacePrefix + "Conversion.ArgumentConverterAttribute"; - public const string KeyConverter = NamespacePrefix + "Conversion.KeyConverterAttribute"; - public const string ValueConverter = NamespacePrefix + "Conversion.ValueConverterAttribute"; - - public const string Description = "System.ComponentModel.DescriptionAttribute"; -} diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 94268161..7f7db5dd 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -9,28 +9,17 @@ namespace Ookii.CommandLine.Generator; internal static class Extensions { - public static bool DerivesFrom(this ITypeSymbol symbol, string baseClassName) + public static bool DerivesFrom(this ITypeSymbol symbol, ITypeSymbol? baseClass) { - var current = symbol; - while (current != null) + if (baseClass == null) { - if (current.ToDisplayString() == baseClassName) - { - return true; - } - - current = current.BaseType; + return false; } - return false; - } - - public static bool DerivesFrom(this ITypeSymbol type, ITypeSymbol baseClass) - { - var current = type; + var current = symbol; while (current != null) { - if (SymbolEqualityComparer.Default.Equals(current, baseClass)) + if (current.SymbolEquals(baseClass)) { return true; } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 206ae171..78a4548c 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -81,16 +81,16 @@ private void GenerateProvider() List? classValidators = null; foreach (var attribute in _argumentsClass.GetAttributes()) { - if (CheckAttribute(attribute, AttributeNames.ParseOptions, ref parseOptions) || - CheckAttribute(attribute, AttributeNames.Description, ref description) || - CheckAttribute(attribute, AttributeNames.ApplicationFriendlyName, ref applicationFriendlyName) || - CheckAttribute(attribute, AttributeNames.Command, ref commandAttribute) || - CheckAttribute(attribute, AttributeNames.ClassValidation, ref classValidators)) + if (CheckAttribute(attribute, _typeHelper.ParseOptionsAttribute, ref parseOptions) || + CheckAttribute(attribute, _typeHelper.DescriptionAttribute, ref description) || + CheckAttribute(attribute, _typeHelper.ApplicationFriendlyNameAttribute, ref applicationFriendlyName) || + CheckAttribute(attribute, _typeHelper.CommandAttribute, ref commandAttribute) || + CheckAttribute(attribute, _typeHelper.ClassValidationAttribute, ref classValidators)) { continue; } - if (!attribute.AttributeClass?.DerivesFrom(AttributeNames.GeneratedParser) ?? false) + if (!attribute.AttributeClass?.DerivesFrom(_typeHelper.GeneratedParserAttribute) ?? false) { _context.ReportDiagnostic(Diagnostics.UnknownAttribute(attribute)); } @@ -158,17 +158,17 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) List? validators = null; foreach (var attribute in member.GetAttributes()) { - if (CheckAttribute(attribute, AttributeNames.CommandLineArgument, ref commandLineArgumentAttribute) || - CheckAttribute(attribute, AttributeNames.MultiValueSeparator, ref multiValueSeparator) || - CheckAttribute(attribute, AttributeNames.Description, ref description) || - CheckAttribute(attribute, AttributeNames.AllowDuplicateDictionaryKeys, ref allowDuplicateDictionaryKeys) || - CheckAttribute(attribute, AttributeNames.KeyValueSeparator, ref keyValueSeparator) || - CheckAttribute(attribute, AttributeNames.ArgumentConverter, ref converterAttribute) || - CheckAttribute(attribute, AttributeNames.KeyConverter, ref keyConverterAttribute) || - CheckAttribute(attribute, AttributeNames.ValueConverter, ref valueConverterAttribute) || - CheckAttribute(attribute, AttributeNames.Alias, ref aliases) || - CheckAttribute(attribute, AttributeNames.ShortAlias, ref shortAliases) || - CheckAttribute(attribute, AttributeNames.ArgumentValidation, ref validators)) + if (CheckAttribute(attribute, _typeHelper.CommandLineArgumentAttribute, ref commandLineArgumentAttribute) || + CheckAttribute(attribute, _typeHelper.MultiValueSeparatorAttribute, ref multiValueSeparator) || + CheckAttribute(attribute, _typeHelper.DescriptionAttribute, ref description) || + CheckAttribute(attribute, _typeHelper.AllowDuplicateDictionaryKeysAttribute, ref allowDuplicateDictionaryKeys) || + CheckAttribute(attribute, _typeHelper.KeyValueSeparatorAttribute, ref keyValueSeparator) || + CheckAttribute(attribute, _typeHelper.ArgumentConverterAttribute, ref converterAttribute) || + CheckAttribute(attribute, _typeHelper.KeyConverterAttribute, ref keyConverterAttribute) || + CheckAttribute(attribute, _typeHelper.ValueConverterAttribute, ref valueConverterAttribute) || + CheckAttribute(attribute, _typeHelper.AliasAttribute, ref aliases) || + CheckAttribute(attribute, _typeHelper.ShortAliasAttribute, ref shortAliases) || + CheckAttribute(attribute, _typeHelper.ArgumentValidationAttribute, ref validators)) { continue; } @@ -403,9 +403,9 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) } // Using a ref parameter with bool return allows me to chain these together. - private static bool CheckAttribute(AttributeData data, string name, ref AttributeData? attribute) + private static bool CheckAttribute(AttributeData data, ITypeSymbol? attributeType, ref AttributeData? attribute) { - if (attribute != null || !(data.AttributeClass?.DerivesFrom(name) ?? false)) + if (attribute != null || !(data.AttributeClass?.DerivesFrom(attributeType) ?? false)) { return false; } @@ -415,9 +415,9 @@ private static bool CheckAttribute(AttributeData data, string name, ref Attribut } // Using a ref parameter with bool return allows me to chain these together. - private static bool CheckAttribute(AttributeData data, string name, ref List? attributes) + private static bool CheckAttribute(AttributeData data, ITypeSymbol? attributeType, ref List? attributes) { - if (!(data.AttributeClass?.DerivesFrom(name) ?? false)) + if (!(data.AttributeClass?.DerivesFrom(attributeType) ?? false)) { return false; } diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index de392279..416e3cbb 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -83,6 +83,7 @@ private static void Execute(Compilation compilation, ImmutableArray _compilation.GetTypeByMetadataName(typeof(ICollection<>).FullName); + public INamedTypeSymbol? DescriptionAttribute => _compilation.GetTypeByMetadataName("System.ComponentModel.DescriptionAttribute"); + public INamedTypeSymbol? ISpanParsable => _compilation.GetTypeByMetadataName("System.ISpanParsable`1"); public INamedTypeSymbol? IParsable => _compilation.GetTypeByMetadataName("System.IParsable`1"); @@ -27,4 +29,35 @@ public TypeHelper(Compilation compilation) 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? CommandAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.CommandAttribute"); + + public INamedTypeSymbol? ClassValidationAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Validation.ClassValidationAttribute"); + + public INamedTypeSymbol? MultiValueSeparatorAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "MultiValueSeparatorAttribute"); + + public INamedTypeSymbol? KeyValueSeparatorAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.KeyValueSeparatorAttribute"); + + public INamedTypeSymbol? AllowDuplicateDictionaryKeysAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "AllowDuplicateDictionaryKeysAttribute"); + + public INamedTypeSymbol? AliasAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "AliasAttribute"); + + public INamedTypeSymbol? ShortAliasAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "ShortAliasAttribute"); + + public INamedTypeSymbol? ArgumentValidationAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Validation.ArgumentValidationAttribute"); + + public INamedTypeSymbol? ArgumentConverterAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.ArgumentConverterAttribute" + ); + public INamedTypeSymbol? KeyConverterAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.KeyConverterAttribute"); + + public INamedTypeSymbol? ValueConverterAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.ValueConverterAttribute"); + } From 75e0bea3a99d7a92e3f073ae12b8c52678da831b Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 11 Apr 2023 21:12:43 -0700 Subject: [PATCH 031/234] Improved diagnostic helper. --- .../Diagnostics.cs | 150 +++++++++--------- src/Ookii.CommandLine/IParser.cs | 3 +- 2 files changed, 73 insertions(+), 80 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 6ada7924..9e5ca0b2 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -11,93 +11,87 @@ internal static class Diagnostics { private const string Category = "Ookii.CommandLine"; - public static Diagnostic ArgumentsTypeNotReferenceType(INamedTypeSymbol symbol) => Diagnostic.Create( - new DiagnosticDescriptor( - "CL1001", - new LocalizableResourceString(nameof(Resources.ArgumentsTypeNotReferenceTypeTitle), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.ArgumentsTypeNotReferenceTypeMessageFormat), Resources.ResourceManager, typeof(Resources)), - Category, - DiagnosticSeverity.Error, - isEnabledByDefault: true), - symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic ArgumentsTypeNotReferenceType(INamedTypeSymbol symbol) => CreateDiagnostic( + "CL1001", + nameof(Resources.ArgumentsTypeNotReferenceTypeTitle), + nameof(Resources.ArgumentsTypeNotReferenceTypeMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); - public static Diagnostic ArgumentsClassNotPartial(INamedTypeSymbol symbol) => Diagnostic.Create( - new DiagnosticDescriptor( - "CL1002", - new LocalizableResourceString(nameof(Resources.ArgumentsClassNotPartialTitle), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.ArgumentsClassNotPartialMessageFormat), Resources.ResourceManager, typeof(Resources)), - Category, - DiagnosticSeverity.Error, - isEnabledByDefault: true), - symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic ArgumentsClassNotPartial(INamedTypeSymbol symbol) => CreateDiagnostic( + "CL1002", + nameof(Resources.ArgumentsClassNotPartialTitle), + nameof(Resources.ArgumentsClassNotPartialMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); - public static Diagnostic ArgumentsClassIsGeneric(INamedTypeSymbol symbol) => Diagnostic.Create( - new DiagnosticDescriptor( - "CL1003", - new LocalizableResourceString(nameof(Resources.ArgumentsClassIsGenericTitle), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.ArgumentsClassIsGenericMessageFormat), Resources.ResourceManager, typeof(Resources)), - Category, - DiagnosticSeverity.Error, - isEnabledByDefault: true), - symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic ArgumentsClassIsGeneric(INamedTypeSymbol symbol) => CreateDiagnostic( + "CL1003", + nameof(Resources.ArgumentsClassIsGenericTitle), + nameof(Resources.ArgumentsClassIsGenericMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); - public static Diagnostic InvalidArrayRank(IPropertySymbol property) => Diagnostic.Create( - new DiagnosticDescriptor( - "CL1004", - new LocalizableResourceString(nameof(Resources.InvalidArrayRankTitle), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.InvalidArrayRankMessageFormat), Resources.ResourceManager, typeof(Resources)), - Category, - DiagnosticSeverity.Error, - isEnabledByDefault: true), - property.Locations.FirstOrDefault(), property.ContainingType?.ToDisplayString(), property.Name); + public static Diagnostic InvalidArrayRank(IPropertySymbol property) => CreateDiagnostic( + "CL1004", + nameof(Resources.InvalidArrayRankTitle), + nameof(Resources.InvalidArrayRankMessageFormat), + DiagnosticSeverity.Error, + property.Locations.FirstOrDefault(), + property.ContainingType?.ToDisplayString(), + property.Name); - public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => Diagnostic.Create( - new DiagnosticDescriptor( - "CL1005", - new LocalizableResourceString(nameof(Resources.PropertyIsReadOnlyTitle), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.PropertyIsReadOnlyMessageFormat), Resources.ResourceManager, typeof(Resources)), - Category, - DiagnosticSeverity.Error, - isEnabledByDefault: true), - property.Locations.FirstOrDefault(), property.ContainingType?.ToDisplayString(), property.Name); + public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => CreateDiagnostic( + "CL1005", + nameof(Resources.PropertyIsReadOnlyTitle), + nameof(Resources.PropertyIsReadOnlyMessageFormat), + DiagnosticSeverity.Error, + property.Locations.FirstOrDefault(), + property.ContainingType?.ToDisplayString(), property.Name); - public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => Diagnostic.Create( - new DiagnosticDescriptor( + public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => CreateDiagnostic( "CL1006", - new LocalizableResourceString(nameof(Resources.NoConverterTitle), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.NoConverterMessageFormat), Resources.ResourceManager, typeof(Resources)), - Category, + nameof(Resources.NoConverterTitle), + nameof(Resources.NoConverterMessageFormat), DiagnosticSeverity.Error, - isEnabledByDefault: true), member.Locations.FirstOrDefault(), elementType.ToDisplayString(), member.ContainingType?.ToDisplayString(), member.Name); - public static Diagnostic InvalidMethodSignature(ISymbol method) => Diagnostic.Create( - new DiagnosticDescriptor( - "CL1007", - new LocalizableResourceString(nameof(Resources.InvalidMethodSignatureTitle), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.InvalidMethodSignatureMessageFormat), Resources.ResourceManager, typeof(Resources)), - Category, - DiagnosticSeverity.Error, - isEnabledByDefault: true), - method.Locations.FirstOrDefault(), method.ContainingType?.ToDisplayString(), method.Name); + public static Diagnostic InvalidMethodSignature(ISymbol method) => CreateDiagnostic( + "CL1007", + nameof(Resources.InvalidMethodSignatureTitle), + nameof(Resources.InvalidMethodSignatureMessageFormat), + DiagnosticSeverity.Error, + method.Locations.FirstOrDefault(), + method.ContainingType?.ToDisplayString(), + method.Name); - public static Diagnostic ArgumentsClassIsNested(INamedTypeSymbol symbol) => Diagnostic.Create( - new DiagnosticDescriptor( - "CL1008", - new LocalizableResourceString(nameof(Resources.ArgumentsClassIsNestedTitle), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.ArgumentsClassIsNestedMessageFormat), Resources.ResourceManager, typeof(Resources)), - Category, - DiagnosticSeverity.Error, - isEnabledByDefault: true), - symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic ArgumentsClassIsNested(INamedTypeSymbol symbol) => CreateDiagnostic( + "CL1008", + nameof(Resources.ArgumentsClassIsNestedTitle), + nameof(Resources.ArgumentsClassIsNestedMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + + public static Diagnostic UnknownAttribute(AttributeData attribute) => CreateDiagnostic( + "CLW1001", + nameof(Resources.UnknownAttributeTitle), + nameof(Resources.UnknownAttributeMessageFormat), + DiagnosticSeverity.Warning, + attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), + attribute.AttributeClass?.Name); - public static Diagnostic UnknownAttribute(AttributeData attribute) => Diagnostic.Create( - new DiagnosticDescriptor( - "CLW1001", - new LocalizableResourceString(nameof(Resources.UnknownAttributeTitle), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.UnknownAttributeMessageFormat), Resources.ResourceManager, typeof(Resources)), - Category, - DiagnosticSeverity.Warning, - isEnabledByDefault: true), - attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), attribute.AttributeClass?.Name); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) + => Diagnostic.Create( + new DiagnosticDescriptor( + id, + new LocalizableResourceString(titleResource, Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(messageResource, Resources.ResourceManager, typeof(Resources)), + Category, + severity, + isEnabledByDefault: true), + location, messageArgs); } diff --git a/src/Ookii.CommandLine/IParser.cs b/src/Ookii.CommandLine/IParser.cs index e2c6c246..9d683ea9 100644 --- a/src/Ookii.CommandLine/IParser.cs +++ b/src/Ookii.CommandLine/IParser.cs @@ -17,8 +17,7 @@ namespace Ookii.CommandLine; /// is used. Classes without that attribute must parse /// arguments using the /// method, or create the parser directly by using the -/// constructor directly; these classes do not support this interface unless it is manually -/// implemented. +/// constructor; these classes do not support this interface unless it is manually implemented. /// /// public interface IParser : IParserProvider From 19597d54350f8dfa6bbe20e88a6bda5a2601db44 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 11 Apr 2023 21:13:24 -0700 Subject: [PATCH 032/234] Formatting. --- src/Ookii.CommandLine.Generator/Diagnostics.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 9e5ca0b2..36d3c83f 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -53,11 +53,14 @@ public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => CreateD property.ContainingType?.ToDisplayString(), property.Name); public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => CreateDiagnostic( - "CL1006", - nameof(Resources.NoConverterTitle), - nameof(Resources.NoConverterMessageFormat), - DiagnosticSeverity.Error, - member.Locations.FirstOrDefault(), elementType.ToDisplayString(), member.ContainingType?.ToDisplayString(), member.Name); + "CL1006", + nameof(Resources.NoConverterTitle), + nameof(Resources.NoConverterMessageFormat), + DiagnosticSeverity.Error, + member.Locations.FirstOrDefault(), + elementType.ToDisplayString(), + member.ContainingType?.ToDisplayString(), + member.Name); public static Diagnostic InvalidMethodSignature(ISymbol method) => CreateDiagnostic( "CL1007", From 3cc7f57ee1f84bc37673b9159ef14ae23d7fc780 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 12 Apr 2023 11:26:07 -0700 Subject: [PATCH 033/234] Add test for derived arguments classes. --- .../ParserGenerator.cs | 10 ++++++++-- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 13 +++++++++++++ .../CommandLineParserTest.cs | 15 +++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 78a4548c..368c5837 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -125,9 +125,15 @@ private void GenerateProvider() _builder.AppendLine("public override System.Collections.Generic.IEnumerable GetArguments(Ookii.CommandLine.CommandLineParser parser)"); _builder.OpenBlock(); - foreach (var member in _argumentsClass.GetMembers()) + var current = _argumentsClass; + while (current != null && current.SpecialType != SpecialType.System_Object) { - GenerateArgument(member); + foreach (var member in current.GetMembers()) + { + GenerateArgument(member); + } + + current = current.BaseType; } // Makes sure the function compiles if there are no arguments. diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 85388e5e..ead17c53 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -567,4 +567,17 @@ partial class ConversionArguments [CommandLineArgument] public int? Nullable { get; set; } } + + class BaseArguments + { + [CommandLineArgument] + public string BaseArg { get; set; } + } + + [GeneratedParser] + partial class DerivedArguments : BaseArguments + { + [CommandLineArgument] + public int DerivedArg { get; set; } + } } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index f53b1a3d..06d8883b 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1119,6 +1119,21 @@ public void TestConversion(ArgumentProviderKind kind) Assert.AreEqual(4, result.ParseNullableMulti[2].Value.Value); } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestDerivedClass(ArgumentProviderKind kind) + { + var parser = CreateParser(kind); + Assert.AreEqual(4, parser.Arguments.Count); + VerifyArguments(parser.Arguments, new[] + { + 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 }, + }); + } + private class ExpectedArgument { public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind.SingleValue) From cedf6bbaf16a454b6bf8d4c5cff366c9e1a25d58 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 12 Apr 2023 12:16:14 -0700 Subject: [PATCH 034/234] Support injection. --- src/Ookii.CommandLine.Generator/Extensions.cs | 14 ++++++++ .../ParserGenerator.cs | 11 +++++-- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 3 +- .../CommandLineParserTest.cs | 33 ++++++++++--------- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 7f7db5dd..916defa7 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -142,4 +142,18 @@ public static string ToIdentifier(this string displayName, string suffix) builder.Append(suffix); return builder.ToString(); } + + public static IMethodSymbol? FindConstructor(this INamedTypeSymbol type, ITypeSymbol? argumentType) + { + foreach (var ctor in type.Constructors) + { + if (!ctor.IsStatic && ctor.DeclaredAccessibility == Accessibility.Public && ctor.Parameters.Length == 1 && + ctor.Parameters[0].Type.SymbolEquals(argumentType)) + { + return ctor; + } + } + + return null; + } } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 368c5837..accf9e5e 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -119,8 +119,15 @@ private void GenerateProvider() // TODO: IsCommand _builder.AppendLine("public override bool IsCommand => false;"); _builder.AppendLine(); - // TODO: Injection - _builder.AppendLine($"public override object CreateInstance(Ookii.CommandLine.CommandLineParser parser) => new {_argumentsClass.Name}();"); + if (_argumentsClass.FindConstructor(_typeHelper.CommandLineParser) != null) + { + _builder.AppendLine($"public override object CreateInstance(Ookii.CommandLine.CommandLineParser parser) => new {_argumentsClass.Name}(parser);"); + } + else + { + _builder.AppendLine($"public override object CreateInstance(Ookii.CommandLine.CommandLineParser parser) => new {_argumentsClass.Name}();"); + } + _builder.AppendLine(); _builder.AppendLine("public override System.Collections.Generic.IEnumerable GetArguments(Ookii.CommandLine.CommandLineParser parser)"); _builder.OpenBlock(); diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index ead17c53..72287708 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -462,7 +462,8 @@ partial class MultiValueWhiteSpaceArguments public bool[] MultiSwitch { get; set; } } - class InjectionArguments + [GeneratedParser] + partial class InjectionArguments { private readonly CommandLineParser _parser; diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 06d8883b..46aa5716 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1028,22 +1028,23 @@ public void TestMultiValueWhiteSpaceSeparator(ArgumentProviderKind kind) CheckThrows(() => parser.Parse(new[] { "1", "-Multi:2", "2", "3", "4", "-Other", "5", "6" }), parser, CommandLineArgumentErrorCategory.TooManyArguments); } - // TODO: - //[TestMethod] - //public void TestInjection() - //{ - // 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); - //} + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestInjection(ArgumentProviderKind kind) + { + var parser = CreateParser(kind); + var result = parser.Parse(new[] { "-Arg", "1" }); + Assert.AreSame(parser, result.Parser); + Assert.AreEqual(1, result.Arg); + + // TODO: + //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); + } [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] From be01db5f60a84831563cdb988e489e214beeaa7e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 12 Apr 2023 12:23:18 -0700 Subject: [PATCH 035/234] Exception handling for generated converters. --- .../ConverterGenerator.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 1e95c16e..97bce78e 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -188,17 +188,37 @@ private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, Con builder.OpenBlock(); string inputType = info.UseSpan ? "System.ReadOnlySpan" : "string"; string culture = info.HasCulture ? ", culture" : string.Empty; + builder.AppendLine($"public override object? Convert({inputType} value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument)"); + builder.OpenBlock(); + builder.AppendLine("try"); + builder.OpenBlock(); if (info.ParseMethod) { - builder.AppendLine($"public override object? Convert({inputType} value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => {type.ToDisplayString()}.Parse(value{culture});"); + builder.AppendLine($"return {type.ToDisplayString()}.Parse(value{culture});"); } else { - builder.AppendLine($"public override object? Convert({inputType} value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => new {type.ToDisplayString()}(value);"); + builder.AppendLine($"return new {type.ToDisplayString()}(value);"); } + builder.CloseBlock(); // try + builder.AppendLine("catch (Ookii.CommandLine.CommandLineArgumentException ex)"); + builder.OpenBlock(); + // Patch the exception with the argument name. + builder.AppendLine("throw new CommandLineArgumentException(ex.Message, argument.ArgumentName, ex.Category, ex.InnerException);"); + builder.CloseBlock(); // catch + builder.AppendLine("catch (System.FormatException)"); + builder.OpenBlock(); + builder.AppendLine("throw;"); + builder.CloseBlock(); // catch + builder.AppendLine("catch (System.Exception ex)"); + builder.OpenBlock(); + builder.AppendLine("throw new System.FormatException(ex.Message, ex);"); + builder.CloseBlock(); // catch + builder.CloseBlock(); // Convert method if (info.UseSpan) { + builder.AppendLine(); builder.AppendLine("public override object? Convert(string value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument) => Convert(System.MemoryExtensions.AsSpan(value), culture, argument);"); } From da9b3ee48a22c464fd1451641a6a5c31f9c58940 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 12 Apr 2023 14:08:48 -0700 Subject: [PATCH 036/234] Warning for private or (non-)static members. --- .../Diagnostics.cs | 39 +- .../ParserGenerator.cs | 15 +- .../Properties/Resources.Designer.cs | 66 +- .../Properties/Resources.resx | 42 +- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 916 +++++++++--------- .../Conversion/ParseConverter.cs | 63 +- 6 files changed, 610 insertions(+), 531 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 36d3c83f..4f4e450c 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -12,7 +12,7 @@ internal static class Diagnostics private const string Category = "Ookii.CommandLine"; public static Diagnostic ArgumentsTypeNotReferenceType(INamedTypeSymbol symbol) => CreateDiagnostic( - "CL1001", + "CL0001", nameof(Resources.ArgumentsTypeNotReferenceTypeTitle), nameof(Resources.ArgumentsTypeNotReferenceTypeMessageFormat), DiagnosticSeverity.Error, @@ -20,7 +20,7 @@ public static Diagnostic ArgumentsTypeNotReferenceType(INamedTypeSymbol symbol) symbol.ToDisplayString()); public static Diagnostic ArgumentsClassNotPartial(INamedTypeSymbol symbol) => CreateDiagnostic( - "CL1002", + "CL0002", nameof(Resources.ArgumentsClassNotPartialTitle), nameof(Resources.ArgumentsClassNotPartialMessageFormat), DiagnosticSeverity.Error, @@ -28,7 +28,7 @@ public static Diagnostic ArgumentsClassNotPartial(INamedTypeSymbol symbol) => Cr symbol.ToDisplayString()); public static Diagnostic ArgumentsClassIsGeneric(INamedTypeSymbol symbol) => CreateDiagnostic( - "CL1003", + "CL0003", nameof(Resources.ArgumentsClassIsGenericTitle), nameof(Resources.ArgumentsClassIsGenericMessageFormat), DiagnosticSeverity.Error, @@ -36,7 +36,7 @@ public static Diagnostic ArgumentsClassIsGeneric(INamedTypeSymbol symbol) => Cre symbol.ToDisplayString()); public static Diagnostic InvalidArrayRank(IPropertySymbol property) => CreateDiagnostic( - "CL1004", + "CL0004", nameof(Resources.InvalidArrayRankTitle), nameof(Resources.InvalidArrayRankMessageFormat), DiagnosticSeverity.Error, @@ -45,7 +45,7 @@ public static Diagnostic InvalidArrayRank(IPropertySymbol property) => CreateDia property.Name); public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => CreateDiagnostic( - "CL1005", + "CL0005", nameof(Resources.PropertyIsReadOnlyTitle), nameof(Resources.PropertyIsReadOnlyMessageFormat), DiagnosticSeverity.Error, @@ -53,7 +53,7 @@ public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => CreateD property.ContainingType?.ToDisplayString(), property.Name); public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => CreateDiagnostic( - "CL1006", + "CL0006", nameof(Resources.NoConverterTitle), nameof(Resources.NoConverterMessageFormat), DiagnosticSeverity.Error, @@ -63,7 +63,7 @@ public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => member.Name); public static Diagnostic InvalidMethodSignature(ISymbol method) => CreateDiagnostic( - "CL1007", + "CL0007", nameof(Resources.InvalidMethodSignatureTitle), nameof(Resources.InvalidMethodSignatureMessageFormat), DiagnosticSeverity.Error, @@ -72,7 +72,7 @@ public static Diagnostic InvalidMethodSignature(ISymbol method) => CreateDiagnos method.Name); public static Diagnostic ArgumentsClassIsNested(INamedTypeSymbol symbol) => CreateDiagnostic( - "CL1008", + "CL0008", nameof(Resources.ArgumentsClassIsNestedTitle), nameof(Resources.ArgumentsClassIsNestedMessageFormat), DiagnosticSeverity.Error, @@ -80,13 +80,31 @@ public static Diagnostic ArgumentsClassIsNested(INamedTypeSymbol symbol) => Crea symbol.ToDisplayString()); public static Diagnostic UnknownAttribute(AttributeData attribute) => CreateDiagnostic( - "CLW1001", + "CLW0001", nameof(Resources.UnknownAttributeTitle), nameof(Resources.UnknownAttributeMessageFormat), DiagnosticSeverity.Warning, attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), attribute.AttributeClass?.Name); + public static Diagnostic NonPublicStaticMethod(ISymbol method) => CreateDiagnostic( + "CLW0002", + nameof(Resources.NonPublicStaticMethodTitle), + nameof(Resources.NonPublicStaticMethodMessageFormat), + DiagnosticSeverity.Warning, + method.Locations.FirstOrDefault(), + method.ContainingType?.ToDisplayString(), + method.Name); + + public static Diagnostic NonPublicInstanceProperty(ISymbol property) => CreateDiagnostic( + "CLW0003", + nameof(Resources.NonPublicInstancePropertyTitle), + nameof(Resources.NonPublicInstancePropertyMessageFormat), + DiagnosticSeverity.Warning, + property.Locations.FirstOrDefault(), + property.ContainingType?.ToDisplayString(), + property.Name); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( @@ -95,6 +113,7 @@ private static Diagnostic CreateDiagnostic(string id, string titleResource, stri new LocalizableResourceString(messageResource, Resources.ResourceManager, typeof(Resources)), Category, severity, - isEnabledByDefault: true), + isEnabledByDefault: true, + helpLinkUri: $"https://www.ookii.org/Link/CommandLineGeneratorError#{id}"), location, messageArgs); } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index accf9e5e..0c6a7cfa 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -151,9 +151,8 @@ private void GenerateProvider() private void GenerateArgument(ISymbol member) { - // Check if the member can be an argument. TODO: warning if private. - if (member.DeclaredAccessibility != Accessibility.Public || - member.Kind is not (SymbolKind.Method or SymbolKind.Property)) + // This shouldn't happen because of attribute targets, but check anyway. + if (member.Kind is not (SymbolKind.Method or SymbolKind.Property)) { return; } @@ -200,9 +199,9 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) var property = member as IPropertySymbol; if (property != null) { - if (property.IsStatic) + if (property.DeclaredAccessibility != Accessibility.Public || property.IsStatic) { - // TODO: Warning or error? + _context.ReportDiagnostic(Diagnostics.NonPublicInstanceProperty(property)); return; } @@ -210,6 +209,12 @@ member.Kind is not (SymbolKind.Method or SymbolKind.Property)) } else if (member is IMethodSymbol method) { + if (method.DeclaredAccessibility != Accessibility.Public || !method.IsStatic) + { + _context.ReportDiagnostic(Diagnostics.NonPublicStaticMethod(method)); + return; + } + methodInfo = DetermineMethodArgumentInfo(method); if (methodInfo is not MethodArgumentInfo methodInfoValue) { diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 8c478677..e694d9a5 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -61,7 +61,7 @@ internal Resources() { } /// - /// Looks up a localized string similar to The arguments class {0} may not be a generic class when the GeneratedParserAttribute is used.. + /// Looks up a localized string similar to The command line arguments class {0} may not be a generic class when the GeneratedParserAttribute is used.. /// internal static string ArgumentsClassIsGenericMessageFormat { get { @@ -70,7 +70,7 @@ internal static string ArgumentsClassIsGenericMessageFormat { } /// - /// Looks up a localized string similar to The arguments class may not be a generic type.. + /// Looks up a localized string similar to The command line arguments class may not be a generic type.. /// internal static string ArgumentsClassIsGenericTitle { get { @@ -79,7 +79,7 @@ internal static string ArgumentsClassIsGenericTitle { } /// - /// Looks up a localized string similar to The arguments class {0} may not be nested in another type when the GeneratedParserAttribute is used.. + /// Looks up a localized string similar to The command line arguments class {0} may not be nested in another type when the GeneratedParserAttribute is used.. /// internal static string ArgumentsClassIsNestedMessageFormat { get { @@ -88,7 +88,7 @@ internal static string ArgumentsClassIsNestedMessageFormat { } /// - /// Looks up a localized string similar to The arguments class may not be a nested type.. + /// Looks up a localized string similar to The command line arguments class may not be a nested type.. /// internal static string ArgumentsClassIsNestedTitle { get { @@ -97,7 +97,7 @@ internal static string ArgumentsClassIsNestedTitle { } /// - /// Looks up a localized string similar to The arguments class {0} must use the 'partial' modifier.. + /// Looks up a localized string similar to The command line arguments class {0} must use the 'partial' modifier.. /// internal static string ArgumentsClassNotPartialMessageFormat { get { @@ -106,7 +106,7 @@ internal static string ArgumentsClassNotPartialMessageFormat { } /// - /// Looks up a localized string similar to The arguments class must be a partial class.. + /// Looks up a localized string similar to The command line arguments class must be a partial class.. /// internal static string ArgumentsClassNotPartialTitle { get { @@ -115,7 +115,7 @@ internal static string ArgumentsClassNotPartialTitle { } /// - /// Looks up a localized string similar to The arguments type {0} must be a reference type (class).. + /// Looks up a localized string similar to The command line arguments type {0} must be a reference type (class).. /// internal static string ArgumentsTypeNotReferenceTypeMessageFormat { get { @@ -124,7 +124,7 @@ internal static string ArgumentsTypeNotReferenceTypeMessageFormat { } /// - /// Looks up a localized string similar to The arguments type must be a reference type.. + /// Looks up a localized string similar to The command line arguments type must be a reference type.. /// internal static string ArgumentsTypeNotReferenceTypeTitle { get { @@ -133,7 +133,7 @@ internal static string ArgumentsTypeNotReferenceTypeTitle { } /// - /// Looks up a localized string similar to The multi-value argument defined by {0}.{1} must have an array rank of one.. + /// Looks up a localized string similar to The multi-value command line argument defined by {0}.{1} must have an array rank of one.. /// internal static string InvalidArrayRankMessageFormat { get { @@ -142,7 +142,7 @@ internal static string InvalidArrayRankMessageFormat { } /// - /// Looks up a localized string similar to A multi-value argument defined by an array properties must have an array rank of one.. + /// 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 { @@ -151,7 +151,7 @@ internal static string InvalidArrayRankTitle { } /// - /// Looks up a localized string similar to The method {0}.{1} does not have a valid signature for a method argument.. + /// Looks up a localized string similar to The method {0}.{1} does not have a valid signature for a command line argument.. /// internal static string InvalidMethodSignatureMessageFormat { get { @@ -160,7 +160,7 @@ internal static string InvalidMethodSignatureMessageFormat { } /// - /// Looks up a localized string similar to A method argument has an invalid signature.. + /// Looks up a localized string similar to A method command line argument has an invalid signature.. /// internal static string InvalidMethodSignatureTitle { get { @@ -169,7 +169,7 @@ internal static string InvalidMethodSignatureTitle { } /// - /// Looks up a localized string similar to No argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter.. + /// Looks up a localized string similar to No command line argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter.. /// internal static string NoConverterMessageFormat { get { @@ -178,7 +178,7 @@ internal static string NoConverterMessageFormat { } /// - /// Looks up a localized string similar to No argument converter exists for the argument's type.. + /// Looks up a localized string similar to No acommand line rgument converter exists for the argument's type.. /// internal static string NoConverterTitle { get { @@ -186,6 +186,42 @@ internal static string NoConverterTitle { } } + /// + /// Looks up a localized string similar to The property {0}.{1} 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}.{1} 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 property {0}.{1} must have a public set accessor.. /// @@ -196,7 +232,7 @@ internal static string PropertyIsReadOnlyMessageFormat { } /// - /// Looks up a localized string similar to An argument property must have a public set accessor.. + /// Looks up a localized string similar to A command line argument property must have a public set accessor.. /// internal static string PropertyIsReadOnlyTitle { get { diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index c6402bd1..163c21b6 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -118,52 +118,64 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - The arguments class {0} may not be a generic class when the GeneratedParserAttribute is used. + The command line arguments class {0} may not be a generic class when the GeneratedParserAttribute is used. - The arguments class may not be a generic type. + The command line arguments class may not be a generic type. - The arguments class {0} may not be nested in another type when the GeneratedParserAttribute is used. + The command line arguments class {0} may not be nested in another type when the GeneratedParserAttribute is used. - The arguments class may not be a nested type. + The command line arguments class may not be a nested type. - The arguments class {0} must use the 'partial' modifier. + The command line arguments class {0} must use the 'partial' modifier. - The arguments class must be a partial class. + The command line arguments class must be a partial class. - The arguments type {0} must be a reference type (class). + The command line arguments type {0} must be a reference type (class). - The arguments type must be a reference type. + The command line arguments type must be a reference type. - The multi-value argument defined by {0}.{1} must have an array rank of one. + The multi-value command line argument defined by {0}.{1} must have an array rank of one. - A multi-value argument defined by an array properties 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 method {0}.{1} does not have a valid signature for a method argument. + The method {0}.{1} does not have a valid signature for a command line argument. - A method argument has an invalid signature. + A method command line argument has an invalid signature. - No argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter. + No command line argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter. - No argument converter exists for the argument's type. + No acommand line rgument converter exists for the argument's type. + + + The property {0}.{1} 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}.{1} 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 property {0}.{1} must have a public set accessor. - An argument property must have a public set accessor. + A command line argument property must have a public set accessor. The attribute {0} is unknown and will be ignored by the GeneratedParserAttribute. diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 72287708..b661b5b0 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -8,577 +8,585 @@ using System.IO; using System.Net; -namespace Ookii.CommandLine.Tests +// We deliberately have some properties and methods that don't generate arguments, so disable the +// warnings for them from the generator. +#pragma warning disable CLW0002,CLW0003 + +namespace Ookii.CommandLine.Tests; + +[GeneratedParser] +partial class EmptyArguments { - [GeneratedParser] - partial 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(); +[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("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("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("notSwitch", Position = 3, DefaultValue = false)] + public bool NotSwitch { get; set; } - [CommandLineArgument()] - public string Arg3 { get; set; } + [CommandLineArgument()] + public string Arg3 { get; set; } - // Default value is intentionally a string to test default value conversion. - [CommandLineArgument("other2", DefaultValue = "47", ValueDescription = "Number", Position = 5), Description("Arg4 description.")] - [ValidateRange(0, 1000, IncludeInUsageHelp = false)] - public int Arg4 { get; set; } + // Default value is intentionally a string to test default value conversion. + [CommandLineArgument("other2", DefaultValue = "47", ValueDescription = "Number", Position = 5), Description("Arg4 description.")] + [ValidateRange(0, 1000, IncludeInUsageHelp = false)] + public int Arg4 { get; set; } - // Short/long name stuff should be ignored if not using LongShort mode. - [CommandLineArgument(Position = 4, ShortName = 'a', IsLong = false), Description("Arg5 description.")] - public float Arg5 { get; set; } + // Short/long name stuff should be ignored if not using LongShort mode. + [CommandLineArgument(Position = 4, ShortName = 'a', IsLong = 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("Alias1")] + [Alias("Alias2")] + [CommandLineArgument(IsRequired = true), Description("Arg6 description.")] + public string Arg6 { get; set; } - [Alias("Alias3")] - [CommandLineArgument()] - public bool Arg7 { get; set; } + [Alias("Alias3")] + [CommandLineArgument()] + public bool Arg7 { get; set; } - [CommandLineArgument(Position = 6)] - public DayOfWeek[] Arg8 { get; set; } + [CommandLineArgument(Position = 6)] + public DayOfWeek[] Arg8 { get; set; } - [CommandLineArgument()] - [ValidateRange(0, 100)] - public int? Arg9 { get; set; } + [CommandLineArgument()] + [ValidateRange(0, 100)] + public int? Arg9 { get; set; } - [CommandLineArgument] - public bool[] Arg10 { get; set; } + [CommandLineArgument] + public bool[] Arg10 { get; set; } - [CommandLineArgument] - public bool? Arg11 { get; set; } + [CommandLineArgument] + public bool? Arg11 { get; set; } - [CommandLineArgument(DefaultValue = 42)] // Default value is ignored for collection types. - public Collection Arg12 - { - get { return _arg12; } - } + [CommandLineArgument(DefaultValue = 42)] // Default value is ignored for collection types. + public Collection Arg12 + { + get { return _arg12; } + } - [CommandLineArgument] - public Dictionary Arg13 { get; set; } + [CommandLineArgument] + public Dictionary Arg13 { get; set; } - [CommandLineArgument] - public IDictionary Arg14 - { - get { return _arg14; } - } + [CommandLineArgument] + public IDictionary Arg14 + { + get { return _arg14; } + } - [CommandLineArgument, ArgumentConverter(typeof(KeyValuePairConverter))] - public KeyValuePair Arg15 { get; set; } + [CommandLineArgument, ArgumentConverter(typeof(KeyValuePairConverter))] + public KeyValuePair Arg15 { get; set; } - public string NotAnArg { get; set; } + public string NotAnArg { get; set; } - [CommandLineArgument()] - private string NotAnArg2 { get; set; } + [CommandLineArgument()] + private string NotAnArg2 { get; set; } - [CommandLineArgument()] - public static string NotAnArg3 { get; set; } - } + [CommandLineArgument()] + public static string NotAnArg3 { get; set; } +} - [GeneratedParser] - partial class ThrowingArguments - { - private int _throwingArgument; +[GeneratedParser] +partial class ThrowingArguments +{ + private int _throwingArgument; - [CommandLineArgument] - public int ThrowingArgument + [CommandLineArgument] + public int ThrowingArgument + { + get { return _throwingArgument; } + set { - get { return _throwingArgument; } - set + if (value < 0) { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - _throwingArgument = value; + throw new ArgumentOutOfRangeException(nameof(value)); } - } - } - [GeneratedParser] - partial class ThrowingConstructor - { - public ThrowingConstructor() - { - throw new ArgumentException(); + _throwingArgument = value; } - - [CommandLineArgument] - public int Arg { get; set; } - } - - [GeneratedParser] - partial class DictionaryArguments - { - [CommandLineArgument] - public Dictionary NoDuplicateKeys { get; set; } - [CommandLineArgument, AllowDuplicateDictionaryKeys] - public Dictionary DuplicateKeys { get; set; } - } - - [GeneratedParser] - partial class MultiValueSeparatorArguments - { - [CommandLineArgument] - public string[] NoSeparator { get; set; } - [CommandLineArgument, MultiValueSeparator(",")] - public string[] Separator { get; set; } } +} - [GeneratedParser] - partial class SimpleArguments +[GeneratedParser] +partial class ThrowingConstructor +{ + public ThrowingConstructor() { - [CommandLineArgument] - public string Argument1 { get; set; } - [CommandLineArgument] - public string Argument2 { get; set; } + throw new ArgumentException(); } - [GeneratedParser] - partial class KeyValueSeparatorArguments - { - [CommandLineArgument] - public Dictionary DefaultSeparator { get; set; } - - [CommandLineArgument] - [KeyValueSeparator("<=>")] - public Dictionary CustomSeparator { get; set; } - } + [CommandLineArgument] + public int Arg { get; set; } +} - [GeneratedParser] - partial class CancelArguments - { - [CommandLineArgument] - public string Argument1 { get; set; } +[GeneratedParser] +partial class DictionaryArguments +{ + [CommandLineArgument] + public Dictionary NoDuplicateKeys { get; set; } + [CommandLineArgument, AllowDuplicateDictionaryKeys] + public Dictionary DuplicateKeys { get; set; } +} - [CommandLineArgument] - public string Argument2 { get; set; } +[GeneratedParser] +partial class MultiValueSeparatorArguments +{ + [CommandLineArgument] + public string[] NoSeparator { get; set; } + [CommandLineArgument, MultiValueSeparator(",")] + public string[] Separator { get; set; } +} - [CommandLineArgument] - public bool DoesNotCancel { get; set; } +[GeneratedParser] +partial class SimpleArguments +{ + [CommandLineArgument] + public string Argument1 { get; set; } + [CommandLineArgument] + public string Argument2 { get; set; } +} - [CommandLineArgument(CancelParsing = true)] - public bool DoesCancel { get; set; } - } +[GeneratedParser] +partial class KeyValueSeparatorArguments +{ + [CommandLineArgument] + public Dictionary DefaultSeparator { get; set; } - [GeneratedParser] - [ParseOptions( - Mode = ParsingMode.LongShort, - DuplicateArguments = ErrorMode.Allow, - AllowWhiteSpaceValueSeparator = false, - ArgumentNamePrefixes = new[] { "--", "-" }, - LongArgumentNamePrefix = "---", - CaseSensitive = true, - NameValueSeparator = '=', - AutoHelpArgument = false)] - partial class ParseOptionsArguments - { - [CommandLineArgument] - public string Argument { get; set; } - } + [CommandLineArgument] + [KeyValueSeparator("<=>")] + public Dictionary CustomSeparator { get; set; } +} - [GeneratedParser] - partial class CultureArguments - { - [CommandLineArgument] - public float Argument { get; set; } - } +[GeneratedParser] +partial class CancelArguments +{ + [CommandLineArgument] + public string Argument1 { get; set; } - [GeneratedParser] - [ParseOptions(Mode = ParsingMode.LongShort)] - partial class LongShortArguments - { - [CommandLineArgument, ShortAlias('c')] - [Description("Arg1 description.")] - public int Arg1 { get; set; } + [CommandLineArgument] + public string Argument2 { get; set; } - [CommandLineArgument(ShortName = 'a', Position = 2), ShortAlias('b'), Alias("baz")] - [Description("Arg2 description.")] - public int Arg2 { get; set; } + [CommandLineArgument] + public bool DoesNotCancel { get; set; } - [CommandLineArgument(IsShort = true)] - [Description("Switch1 description.")] - public bool Switch1 { get; set; } + [CommandLineArgument(CancelParsing = true)] + public bool DoesCancel { get; set; } +} - [CommandLineArgument(ShortName = 'k')] - [Description("Switch2 description.")] - public bool Switch2 { get; set; } +[GeneratedParser] +[ParseOptions( + Mode = ParsingMode.LongShort, + DuplicateArguments = ErrorMode.Allow, + AllowWhiteSpaceValueSeparator = false, + ArgumentNamePrefixes = new[] { "--", "-" }, + LongArgumentNamePrefix = "---", + CaseSensitive = true, + NameValueSeparator = '=', + AutoHelpArgument = false)] +partial class ParseOptionsArguments +{ + [CommandLineArgument] + public string Argument { get; set; } +} - [CommandLineArgument(ShortName = 'u', IsLong = false)] - [Description("Switch3 description.")] - public bool Switch3 { get; set; } +[GeneratedParser] +partial class CultureArguments +{ + [CommandLineArgument] + public float Argument { get; set; } +} - [CommandLineArgument("foo", Position = 0, IsShort = true, DefaultValue = 0)] - [Description("Foo description.")] - public int Foo { get; set; } +[GeneratedParser] +[ParseOptions(Mode = ParsingMode.LongShort)] +partial class LongShortArguments +{ + [CommandLineArgument, ShortAlias('c')] + [Description("Arg1 description.")] + public int Arg1 { get; set; } - [CommandLineArgument("bar", DefaultValue = 0, Position = 1)] - [Description("Bar description.")] - public int Bar { get; set; } - } + [CommandLineArgument(ShortName = 'a', Position = 2), ShortAlias('b'), Alias("baz")] + [Description("Arg2 description.")] + public int Arg2 { get; set; } - [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; + [CommandLineArgument(IsShort = true)] + [Description("Switch1 description.")] + public bool Switch1 { get; set; } - [CommandLineArgument] - public static bool NoCancel() - { - CalledMethodName = nameof(NoCancel); - return true; - } + [CommandLineArgument(ShortName = 'k')] + [Description("Switch2 description.")] + public bool Switch2 { get; set; } - [CommandLineArgument] - public static bool Cancel() - { - CalledMethodName = nameof(Cancel); - return false; - } + [CommandLineArgument(ShortName = 'u', IsLong = false)] + [Description("Switch3 description.")] + public bool Switch3 { get; set; } - [CommandLineArgument] - public static bool CancelWithHelp(CommandLineParser parser) - { - CalledMethodName = nameof(CancelWithHelp); - parser.HelpRequested = true; - return false; - } + [CommandLineArgument("foo", Position = 0, IsShort = true, DefaultValue = 0)] + [Description("Foo description.")] + public int Foo { get; set; } - [CommandLineArgument] - public static bool CancelWithValue(int value) - { - CalledMethodName = nameof(CancelWithValue); - Value = value; - return value > 0; - } + [CommandLineArgument("bar", DefaultValue = 0, Position = 1)] + [Description("Bar description.")] + public int Bar { 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 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 static void NoReturn() - { - CalledMethodName = nameof(NoReturn); - } + [CommandLineArgument] + public static bool NoCancel() + { + CalledMethodName = nameof(NoCancel); + return true; + } - [CommandLineArgument(Position = 0)] - public static void Positional(int value) - { - CalledMethodName = nameof(Positional); - Value = value; - } + [CommandLineArgument] + public static bool Cancel() + { + CalledMethodName = nameof(Cancel); + return false; + } - [CommandLineArgument] - private static void NotPublic() - { - } + [CommandLineArgument] + public static bool CancelWithHelp(CommandLineParser parser) + { + CalledMethodName = nameof(CancelWithHelp); + parser.HelpRequested = true; + return false; + } - public static void NotAnArgument() - { - } + [CommandLineArgument] + public static bool CancelWithValue(int value) + { + CalledMethodName = nameof(CancelWithValue); + Value = value; + return value > 0; } - [GeneratedParser] - partial class AutomaticConflictingNameArguments + [CommandLineArgument] + public static bool CancelWithValueAndHelp(int value, CommandLineParser parser) { - [CommandLineArgument] - public int Help { get; set; } + CalledMethodName = nameof(CancelWithValueAndHelp); + Value = value; + // This should be reset to false if parsing continues. + parser.HelpRequested = true; + return value > 0; + } - [CommandLineArgument] - public int Version { get; set; } + [CommandLineArgument] + public static void NoReturn() + { + CalledMethodName = nameof(NoReturn); } - [GeneratedParser] - [ParseOptions(Mode = ParsingMode.LongShort)] - partial class AutomaticConflictingShortNameArguments + [CommandLineArgument(Position = 0)] + public static void Positional(int value) { - [CommandLineArgument(ShortName = '?')] - public int Foo { get; set; } + CalledMethodName = nameof(Positional); + Value = value; } - [GeneratedParser] - partial class HiddenArguments + [CommandLineArgument] + public void NoStatic() { - [CommandLineArgument] - public int Foo { get; set; } + } - [CommandLineArgument(IsHidden = true)] - public int Hidden { get; set; } + [CommandLineArgument] + private static void NotPublic() + { } - [GeneratedParser] - partial class NameTransformArguments + public static void NotAnArgument() { - [CommandLineArgument(Position = 0, IsRequired = true)] - public string testArg { get; set; } + } +} - [CommandLineArgument] - public int TestArg2 { get; set; } +[GeneratedParser] +partial class AutomaticConflictingNameArguments +{ + [CommandLineArgument] + public int Help { get; set; } - [CommandLineArgument] - public int __test__arg3__ { get; set; } + [CommandLineArgument] + public int Version { get; set; } +} - [CommandLineArgument("ExplicitName")] - public int Explicit { get; set; } - } +[GeneratedParser] +[ParseOptions(Mode = ParsingMode.LongShort)] +partial class AutomaticConflictingShortNameArguments +{ + [CommandLineArgument(ShortName = '?')] + public int Foo { get; set; } +} - [GeneratedParser] - partial class ValueDescriptionTransformArguments - { - [CommandLineArgument] - public FileInfo Arg1 { get; set; } +[GeneratedParser] +partial class HiddenArguments +{ + [CommandLineArgument] + public int Foo { get; set; } - [CommandLineArgument] - public int Arg2 { get; set; } - } + [CommandLineArgument(IsHidden = true)] + public int Hidden { get; set; } +} - [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; - } +[GeneratedParser] +partial class NameTransformArguments +{ + [CommandLineArgument(Position = 0, IsRequired = true)] + public string testArg { get; set; } - [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] + public int TestArg2 { get; set; } + + [CommandLineArgument] + public int __test__arg3__ { get; set; } + + [CommandLineArgument("ExplicitName")] + public int Explicit { get; set; } +} + +[GeneratedParser] +partial class ValueDescriptionTransformArguments +{ + [CommandLineArgument] + public FileInfo Arg1 { get; set; } + + [CommandLineArgument] + public int Arg2 { get; set; } +} - [GeneratedParser] - // N.B. nameof is only safe if the argument name matches the property name. - [RequiresAny(nameof(Address), nameof(Path))] - partial class DependencyArguments +[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) { - [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; } + Arg3Value = value; } - [GeneratedParser] - partial class MultiValueWhiteSpaceArguments - { + [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; } +} + +[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(Position = 0)] - public int Arg1 { get; set; } +[GeneratedParser] +partial class MultiValueWhiteSpaceArguments +{ - [CommandLineArgument(Position = 1)] - public int Arg2 { get; set; } + [CommandLineArgument(Position = 0)] + public int Arg1 { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public int[] Multi { get; set; } + [CommandLineArgument(Position = 1)] + public int Arg2 { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public int Other { get; set; } + [CommandLineArgument] + [MultiValueSeparator] + public int[] Multi { get; set; } + [CommandLineArgument] + [MultiValueSeparator] + public int Other { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public bool[] MultiSwitch { get; set; } - } - [GeneratedParser] - partial class InjectionArguments + [CommandLineArgument] + [MultiValueSeparator] + public bool[] MultiSwitch { get; set; } +} + +[GeneratedParser] +partial class InjectionArguments +{ + private readonly CommandLineParser _parser; + + public InjectionArguments(CommandLineParser parser) { - private readonly CommandLineParser _parser; + _parser = parser; + } - public InjectionArguments(CommandLineParser parser) - { - _parser = parser; - } + public CommandLineParser Parser => _parser; - public CommandLineParser Parser => _parser; + [CommandLineArgument] + public int Arg { get; set; } +} - [CommandLineArgument] - public int Arg { get; set; } - } +// TODO: Test with new ctor argument style. +//class InjectionMixedArguments +//{ +// private readonly CommandLineParser _parser; +// private readonly int _arg1; +// private readonly int _arg2; - // TODO: Test with new ctor argument style. - //class InjectionMixedArguments - //{ - // private readonly CommandLineParser _parser; - // private readonly int _arg1; - // private readonly int _arg2; +// public InjectionMixedArguments(int arg1, CommandLineParser parser, int arg2) +// { +// _arg1 = arg1; +// _parser = parser; +// _arg2 = arg2; +// } - // public InjectionMixedArguments(int arg1, CommandLineParser parser, int arg2) - // { - // _arg1 = arg1; - // _parser = parser; - // _arg2 = arg2; - // } +// public CommandLineParser Parser => _parser; - // public CommandLineParser Parser => _parser; +// public int Arg1 => _arg1; - // public int Arg1 => _arg1; +// public int Arg2 => _arg2; - // public int Arg2 => _arg2; +// [CommandLineArgument] +// public int Arg3 { get; set; } +//} - // [CommandLineArgument] - // public int Arg3 { get; set; } - //} +struct StructWithParseCulture +{ + public int Value { get; set; } - struct StructWithParseCulture + public static StructWithParseCulture Parse(string value, IFormatProvider provider) { - public int Value { get; set; } - - public static StructWithParseCulture Parse(string value, IFormatProvider provider) + return new StructWithParseCulture() { - return new StructWithParseCulture() - { - Value = int.Parse(value, provider) - }; - } + Value = int.Parse(value, provider) + }; } +} - struct StructWithParse - { - public int Value { get; set; } +struct StructWithParse +{ + public int Value { get; set; } - public static StructWithParse Parse(string value) + public static StructWithParse Parse(string value) + { + return new StructWithParse() { - return new StructWithParse() - { - Value = int.Parse(value, CultureInfo.InvariantCulture) - }; - } + Value = int.Parse(value, CultureInfo.InvariantCulture) + }; } +} - struct StructWithCtor +struct StructWithCtor +{ + public StructWithCtor(string value) { - public StructWithCtor(string value) - { - Value = int.Parse(value); - } - - public int Value { get; set; } + Value = int.Parse(value); } - [GeneratedParser] - partial class ConversionArguments - { - [CommandLineArgument] - public StructWithParseCulture ParseCulture { get; set; } + public int Value { get; set; } +} + +[GeneratedParser] +partial class ConversionArguments +{ + [CommandLineArgument] + public StructWithParseCulture ParseCulture { get; set; } - [CommandLineArgument] - public StructWithParse ParseStruct { get; set; } + [CommandLineArgument] + public StructWithParse ParseStruct { get; set; } - [CommandLineArgument] - public StructWithCtor Ctor { get; set; } + [CommandLineArgument] + public StructWithCtor Ctor { get; set; } - [CommandLineArgument] - public StructWithParse? ParseNullable { get; set; } + [CommandLineArgument] + public StructWithParse? ParseNullable { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public StructWithParse[] ParseMulti { get; set; } + [CommandLineArgument] + [MultiValueSeparator] + public StructWithParse[] ParseMulti { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public StructWithParse?[] ParseNullableMulti { get; set; } + [CommandLineArgument] + [MultiValueSeparator] + public StructWithParse?[] ParseNullableMulti { get; set; } - [CommandLineArgument] - [MultiValueSeparator] - public int?[] NullableMulti { get; set; } + [CommandLineArgument] + [MultiValueSeparator] + public int?[] NullableMulti { get; set; } - [CommandLineArgument] - public int? Nullable { get; set; } - } + [CommandLineArgument] + public int? Nullable { get; set; } +} - class BaseArguments - { - [CommandLineArgument] - public string BaseArg { get; set; } - } +class BaseArguments +{ + [CommandLineArgument] + public string BaseArg { get; set; } +} - [GeneratedParser] - partial class DerivedArguments : BaseArguments - { - [CommandLineArgument] - public int DerivedArg { get; set; } - } +[GeneratedParser] +partial class DerivedArguments : BaseArguments +{ + [CommandLineArgument] + public int DerivedArg { get; set; } } diff --git a/src/Ookii.CommandLine/Conversion/ParseConverter.cs b/src/Ookii.CommandLine/Conversion/ParseConverter.cs index 34a8edd1..5e9dee63 100644 --- a/src/Ookii.CommandLine/Conversion/ParseConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ParseConverter.cs @@ -2,44 +2,43 @@ using System.Globalization; using System.Reflection; -namespace Ookii.CommandLine.Conversion +namespace Ookii.CommandLine.Conversion; + +internal class ParseConverter : ArgumentConverter { - 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) { - private readonly MethodInfo _method; - private readonly bool _hasCulture; + var parameters = _hasCulture + ? new object?[] { value, culture } + : new object?[] { value }; - public ParseConverter(MethodInfo method, bool hasCulture) + try { - _method = method; - _hasCulture = hasCulture; + return _method.Invoke(null, parameters); } - - public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) + catch (CommandLineArgumentException ex) { - var parameters = _hasCulture - ? new object?[] { value, culture } - : new object?[] { value }; - - try - { - return _method.Invoke(null, parameters); - } - catch (CommandLineArgumentException ex) - { - // Patch the exception with the argument name. - throw new CommandLineArgumentException(ex.Message, argument.ArgumentName, ex.Category, ex.InnerException); - } - catch (FormatException) - { - throw; - } - 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); - } + // Patch the exception with the argument name. + throw new CommandLineArgumentException(ex.Message, argument.ArgumentName, ex.Category, ex.InnerException); + } + catch (FormatException) + { + throw; + } + 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); } } } From 81efafe6f367646378455d59cddd295fbd7a2350 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Sat, 15 Apr 2023 15:27:59 -0700 Subject: [PATCH 037/234] Separate reflection out of CommandInfo. --- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 26 ++-- .../Commands/BasicCommandInfo.cs | 27 ++++ src/Ookii.CommandLine/Commands/CommandInfo.cs | 122 +++++++++--------- .../Commands/CommandManager.cs | 4 +- .../Commands/ReflectionCommandInfo.cs | 80 ++++++++++++ .../Properties/Resources.Designer.cs | 9 ++ .../Properties/Resources.resx | 3 + src/Samples/NestedCommands/ParentCommand.cs | 2 +- 8 files changed, 193 insertions(+), 80 deletions(-) create mode 100644 src/Ookii.CommandLine/Commands/BasicCommandInfo.cs create mode 100644 src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index d11004a7..64cdeeb0 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -38,16 +38,16 @@ 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); + 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.Value.Name); - Assert.AreEqual(typeof(TestCommand), command.Value.CommandType); + Assert.AreEqual("test", command.Name); + Assert.AreEqual(typeof(TestCommand), command.CommandType); var manager2 = new CommandManager(_commandAssembly, new CommandOptions() { CommandNameComparer = StringComparer.Ordinal }); command = manager2.GetCommand("Test"); @@ -55,13 +55,13 @@ public void GetCommandTest() command = manager.GetCommand("AnotherSimpleCommand"); Assert.IsNotNull(command); - Assert.AreEqual("AnotherSimpleCommand", command.Value.Name); - Assert.AreEqual(typeof(AnotherSimpleCommand), command.Value.CommandType); + Assert.AreEqual("AnotherSimpleCommand", command.Name); + Assert.AreEqual(typeof(AnotherSimpleCommand), command.CommandType); command = manager.GetCommand("alias"); Assert.IsNotNull(command); - Assert.AreEqual("AnotherSimpleCommand", command.Value.Name); - Assert.AreEqual(typeof(AnotherSimpleCommand), command.Value.CommandType); + Assert.AreEqual("AnotherSimpleCommand", command.Name); + Assert.AreEqual(typeof(AnotherSimpleCommand), command.CommandType); } [TestMethod] @@ -239,23 +239,23 @@ public void TestCommandNameTransform() }; var manager = new CommandManager(_commandAssembly, options); - var info = new CommandInfo(typeof(AnotherSimpleCommand), manager); + var info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); Assert.AreEqual("AnotherSimple", info.Name); options.CommandNameTransform = NameTransform.CamelCase; - info = new CommandInfo(typeof(AnotherSimpleCommand), manager); + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); Assert.AreEqual("anotherSimple", info.Name); options.CommandNameTransform = NameTransform.SnakeCase; - info = new CommandInfo(typeof(AnotherSimpleCommand), manager); + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); Assert.AreEqual("another_simple", info.Name); options.CommandNameTransform = NameTransform.DashCase; - info = new CommandInfo(typeof(AnotherSimpleCommand), manager); + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); Assert.AreEqual("another-simple", info.Name); options.StripCommandNameSuffix = null; - info = new CommandInfo(typeof(AnotherSimpleCommand), manager); + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); Assert.AreEqual("another-simple-command", info.Name); options.StripCommandNameSuffix = "Command"; diff --git a/src/Ookii.CommandLine/Commands/BasicCommandInfo.cs b/src/Ookii.CommandLine/Commands/BasicCommandInfo.cs new file mode 100644 index 00000000..a10d728a --- /dev/null +++ b/src/Ookii.CommandLine/Commands/BasicCommandInfo.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine.Commands; + +internal class BasicCommandInfo : CommandInfo +{ + private readonly string _description; + + public BasicCommandInfo(Type commandType, string name, string description, CommandManager manager) + : base(commandType, name, manager) + { + _description = description; + } + + public override string? Description => _description; + + public override bool UseCustomArgumentParsing => false; + + public override IEnumerable Aliases => Enumerable.Empty(); + + public override ICommandWithCustomParsing CreateInstanceWithCustomParsing() + => throw new InvalidOperationException(Properties.Resources.NoCustomParsing); +} diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index 4f0cfa94..ed7e3860 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Data; using System.Globalization; using System.Linq; using System.Reflection; @@ -13,18 +14,18 @@ namespace Ookii.CommandLine.Commands /// /// /// - public struct CommandInfo + public abstract class CommandInfo { private readonly CommandManager _manager; private readonly string _name; private readonly Type _commandType; private readonly CommandAttribute _attribute; - private string? _description; /// - /// Initializes a new instance of the structure. + /// Initializes a new instance of the class. /// /// The type that implements the subcommand. + /// The for the subcommand type. /// /// The that is managing this command. /// @@ -34,28 +35,29 @@ public struct CommandInfo /// /// is not a command type. /// - public CommandInfo(Type commandType, CommandManager manager) - : this(commandType, GetCommandAttributeOrThrow(commandType), manager) + protected CommandInfo(Type commandType, CommandAttribute attribute, CommandManager manager) { + _manager = manager ?? throw new ArgumentNullException(nameof(manager)); + _name = GetName(attribute, commandType, manager.Options); + _commandType = commandType; + _attribute = attribute; } - private CommandInfo(string name, Type commandType, string description, CommandManager manager) + internal CommandInfo(Type commandType, string name, CommandManager manager) { _manager = manager; - _attribute = GetCommandAttribute(commandType)!; _name = name; _commandType = commandType; - _description = description; + _attribute = new(); } - 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 that this instance belongs to. + /// + /// + /// An instance of the class. + /// + public CommandManager Manager => _manager; /// /// Gets the name of the command. @@ -88,7 +90,7 @@ private CommandInfo(Type commandType, CommandAttribute attribute, CommandManager /// The description of the command, determined using the /// attribute. /// - public string? Description => _description ??= GetCommandDescription(); + public abstract string? Description { get; } /// /// Gets a value that indicates if the command uses custom parsing. @@ -97,7 +99,7 @@ private CommandInfo(Type commandType, CommandAttribute attribute, CommandManager /// if the command type implements the /// interface; otherwise, . /// - public bool UseCustomArgumentParsing => _commandType.ImplementsInterface(typeof(ICommandWithCustomParsing)); + public abstract bool UseCustomArgumentParsing { get; } /// /// Gets or sets a value that indicates whether the command is hidden from the usage help. @@ -127,7 +129,7 @@ private CommandInfo(Type commandType, CommandAttribute attribute, CommandManager /// class implementing the interface. /// /// - public IEnumerable Aliases => _commandType.GetCustomAttributes().Select(a => a.Alias); + public abstract IEnumerable Aliases { get; } /// /// Creates an instance of the command type. @@ -181,7 +183,7 @@ private CommandInfo(Type commandType, CommandAttribute attribute, CommandManager if (UseCustomArgumentParsing) { - var command = (ICommandWithCustomParsing)Activator.CreateInstance(CommandType)!; + var command = CreateInstanceWithCustomParsing(); command.Parse(args, index, _manager.Options); return (command, default); } @@ -209,16 +211,26 @@ private CommandInfo(Type commandType, CommandAttribute attribute, CommandManager /// must use the method. /// /// - public CommandLineParser CreateParser() + public virtual CommandLineParser CreateParser() { if (UseCustomArgumentParsing) { throw new InvalidOperationException(Properties.Resources.NoParserForCustomParsingCommand); } - return new CommandLineParser(CommandType, _manager.Options); + return new CommandLineParser(CommandType, Manager.Options); } + /// + /// Creates an instance of a command that uses the + /// interface. + /// + /// An instance of the command type. + /// + /// The command does not use the interface. + /// + public abstract ICommandWithCustomParsing CreateInstanceWithCustomParsing(); + /// /// Checks whether the command's name or aliases match the specified name. /// @@ -251,7 +263,7 @@ public bool MatchesName(string name, IComparer? comparer = null) } /// - /// Creates an instance of the structure only if + /// Creates an instance of the class only if /// represents a command type. /// /// The type that implements the subcommand. @@ -262,19 +274,31 @@ public bool MatchesName(string name, IComparer? comparer = null) /// or is . /// /// - /// A structure with information about the command, or + /// A class with information about the command, or /// if was not a command. /// public static CommandInfo? TryCreate(Type commandType, CommandManager manager) - { - var attribute = GetCommandAttribute(commandType); - if (attribute == null) - { - return null; - } + => ReflectionCommandInfo.TryCreate(commandType, manager); - return new CommandInfo(commandType, attribute, manager); - } + /// + /// 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 not a command. + /// + /// + /// A class with information about the command. + /// + public static CommandInfo Create(Type commandType, CommandManager manager) + => new ReflectionCommandInfo(commandType, manager); /// /// Returns a value indicating if the specified type is a subcommand. @@ -287,38 +311,13 @@ public bool MatchesName(string name, IComparer? comparer = null) /// /// is . /// - public static bool IsCommand(Type commandType) - { - return GetCommandAttribute(commandType) != null; - } + public static bool IsCommand(Type commandType) => ReflectionCommandInfo.GetCommandAttribute(commandType) != null; internal static CommandInfo GetAutomaticVersionCommand(CommandManager manager) { var name = manager.Options.AutoVersionCommandName(); var description = manager.Options.StringProvider.AutomaticVersionCommandDescription(); - return new CommandInfo(name, typeof(AutomaticVersionCommand), description, manager); - } - - private static CommandAttribute? GetCommandAttribute(Type commandType) - { - if (commandType == null) - { - throw new ArgumentNullException(nameof(commandType)); - } - - if (commandType.IsAbstract || !commandType.ImplementsInterface(typeof(ICommand))) - { - return null; - } - - return commandType.GetCustomAttribute(); - } - - private static CommandAttribute GetCommandAttributeOrThrow(Type commandType) - { - return GetCommandAttribute(commandType) ?? - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, - Properties.Resources.TypeIsNotCommandFormat, commandType.FullName)); + return new BasicCommandInfo(typeof(AutomaticVersionCommand), name, description, manager); } private static string GetName(CommandAttribute attribute, Type commandType, CommandOptions? options) @@ -327,10 +326,5 @@ private static string GetName(CommandAttribute attribute, Type commandType, Comm options?.CommandNameTransform.Apply(commandType.Name, options.StripCommandNameSuffix) ?? commandType.Name; } - - private string? GetCommandDescription() - { - return _commandType.GetCustomAttribute()?.Description; - } } } diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index f976c61c..ed123626 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -559,8 +559,8 @@ private IEnumerable GetCommandsUnsorted() return from type in types let info = CommandInfo.TryCreate(type, this) - where info != null && (_options.CommandFilter?.Invoke(info.Value) ?? true) - select info.Value; + where info != null && (_options.CommandFilter?.Invoke(info) ?? true) + select info; } } } diff --git a/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs new file mode 100644 index 00000000..41f7fb2d --- /dev/null +++ b/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine.Commands; + +internal class ReflectionCommandInfo : CommandInfo +{ + private string? _description; + + public ReflectionCommandInfo(Type commandType, CommandManager manager) + : base(commandType, GetCommandAttributeOrThrow(commandType), manager) + { + } + + private ReflectionCommandInfo(Type commandType, CommandAttribute attribute, CommandManager manager) + : base(commandType, attribute, manager) + { + } + + 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 ICommandWithCustomParsing CreateInstanceWithCustomParsing() + { + if (!UseCustomArgumentParsing) + { + throw new InvalidOperationException(Properties.Resources.NoCustomParsing); + } + + return (ICommandWithCustomParsing)Activator.CreateInstance(CommandType)!; + } + + internal static CommandAttribute? GetCommandAttribute(Type commandType) + { + if (commandType == null) + { + throw new ArgumentNullException(nameof(commandType)); + } + + if (commandType.IsAbstract || !commandType.ImplementsInterface(typeof(ICommand))) + { + return null; + } + + return commandType.GetCustomAttribute(); + } + + 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; + } +} diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index dfe452de..4244c781 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -456,6 +456,15 @@ internal static string NoConstructor { } } + /// + /// Looks up a localized string similar to The command does not use custom parsing.. + /// + internal static string NoCustomParsing { + get { + return ResourceManager.GetString("NoCustomParsing", resourceCulture); + } + } + /// /// Looks up a localized string similar to A key/value pair must contain "{0}" as a separator.. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index 0ac69b36..9df12732 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -396,4 +396,7 @@ The provided ArgumentProvider is not for the type '{0}'. + + The command does not use custom parsing. + \ No newline at end of file diff --git a/src/Samples/NestedCommands/ParentCommand.cs b/src/Samples/NestedCommands/ParentCommand.cs index 9de64d00..992bd20b 100644 --- a/src/Samples/NestedCommands/ParentCommand.cs +++ b/src/Samples/NestedCommands/ParentCommand.cs @@ -29,7 +29,7 @@ public void Parse(string[] args, int index, CommandOptions options) (command) => command.CommandType.GetCustomAttribute()?.ParentCommand == GetType(); var manager = new CommandManager(options); - var info = new CommandInfo(GetType(), manager); + var info = CommandInfo.Create(GetType(), manager); // Use a custom UsageWriter to replace the application description with the // description of this command. From 61532f5a0c2a06f7f5ba0df6656ac1559ec7a2a4 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Sat, 15 Apr 2023 15:48:27 -0700 Subject: [PATCH 038/234] Separated reflection out of CommandManager. --- .../CommandLineParserNullableTest.cs | 14 +-- .../CommandLineParserTest.cs | 104 +++++++++--------- src/Ookii.CommandLine/CommandLineParser.cs | 4 +- .../Commands/CommandManager.cs | 53 +++------ .../Commands/CommandProvider.cs | 15 +++ .../Commands/ReflectionCommandProvider.cs | 61 ++++++++++ .../Support/ArgumentProvider.cs | 4 +- .../Support/GeneratedArgumentProvider.cs | 2 +- ...rgumentProviderKind.cs => ProviderKind.cs} | 2 +- .../Support/ReflectionArgumentProvider.cs | 2 +- 10 files changed, 156 insertions(+), 105 deletions(-) create mode 100644 src/Ookii.CommandLine/Commands/CommandProvider.cs create mode 100644 src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs rename src/Ookii.CommandLine/Support/{ArgumentProviderKind.cs => ProviderKind.cs} (89%) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs index dd618ef5..5fc9eec7 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -19,7 +19,7 @@ public class CommandLineParserNullableTest { [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestAllowNull(ArgumentProviderKind kind) + public void TestAllowNull(ProviderKind kind) { var parser = CommandLineParserTest.CreateParser(kind); Assert.IsTrue(parser.GetArgument("constructorNullable")!.AllowNull); @@ -53,7 +53,7 @@ public void TestAllowNull(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNonNullableConstructor(ArgumentProviderKind kind) + public void TestNonNullableConstructor(ProviderKind kind) { // TODO: Update for new ctor arguments style. var parser = CommandLineParserTest.CreateParser(kind); @@ -68,7 +68,7 @@ public void TestNonNullableConstructor(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNonNullableProperties(ArgumentProviderKind kind) + public void TestNonNullableProperties(ProviderKind kind) { var parser = CommandLineParserTest.CreateParser(kind); ExpectNullException(parser, "NonNullable", "foo", "bar", "4", "5", "-NonNullable", "(null)"); @@ -82,7 +82,7 @@ public void TestNonNullableProperties(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNonNullableMultiValue(ArgumentProviderKind kind) + public void TestNonNullableMultiValue(ProviderKind kind) { var parser = CommandLineParserTest.CreateParser(kind); ExpectNullException(parser, "NonNullableArray", "-NonNullableArray", "foo", "-NonNullableArray", "(null)"); @@ -111,7 +111,7 @@ public void TestNonNullableMultiValue(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNonNullableDictionary(ArgumentProviderKind kind) + public void TestNonNullableDictionary(ProviderKind kind) { var parser = CommandLineParserTest.CreateParser(kind); ExpectNullException(parser, "NonNullableDictionary", "-NonNullableDictionary", "foo=bar", "-NonNullableDictionary", "baz=(null)"); @@ -171,8 +171,8 @@ public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, obje public static IEnumerable ProviderKinds => new[] { - new object[] { ArgumentProviderKind.Reflection }, - new object[] { ArgumentProviderKind.Generated } + new object[] { ProviderKind.Reflection }, + new object[] { ProviderKind.Generated } }; } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 46aa5716..aa021fa1 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -20,7 +20,7 @@ public partial class CommandLineParserTest { [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ConstructorEmptyArgumentsTest(ArgumentProviderKind kind) + public void ConstructorEmptyArgumentsTest(ProviderKind kind) { Type argumentsType = typeof(EmptyArguments); var target = CreateParser(kind); @@ -44,7 +44,7 @@ public void ConstructorEmptyArgumentsTest(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ConstructorTest(ArgumentProviderKind kind) + public void ConstructorTest(ProviderKind kind) { Type argumentsType = typeof(TestArguments); var target = CreateParser(kind); @@ -83,7 +83,7 @@ public void ConstructorTest(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTest(ArgumentProviderKind kind) + public void ParseTest(ProviderKind kind) { var target = CreateParser(kind); // Only required arguments @@ -112,7 +112,7 @@ public void ParseTest(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestEmptyArguments(ArgumentProviderKind kind) + 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. @@ -121,7 +121,7 @@ public void ParseTestEmptyArguments(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestTooManyArguments(ArgumentProviderKind kind) + public void ParseTestTooManyArguments(ProviderKind kind) { var target = CreateParser(kind); @@ -131,7 +131,7 @@ public void ParseTestTooManyArguments(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestPropertySetterThrows(ArgumentProviderKind kind) + public void ParseTestPropertySetterThrows(ProviderKind kind) { var target = CreateParser(kind); @@ -144,7 +144,7 @@ public void ParseTestPropertySetterThrows(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestConstructorThrows(ArgumentProviderKind kind) + public void ParseTestConstructorThrows(ProviderKind kind) { var target = CreateParser(kind); @@ -157,7 +157,7 @@ public void ParseTestConstructorThrows(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestDuplicateDictionaryKeys(ArgumentProviderKind kind) + public void ParseTestDuplicateDictionaryKeys(ProviderKind kind) { var target = CreateParser(kind); @@ -176,7 +176,7 @@ public void ParseTestDuplicateDictionaryKeys(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestMultiValueSeparator(ArgumentProviderKind kind) + public void ParseTestMultiValueSeparator(ProviderKind kind) { var target = CreateParser(kind); @@ -188,7 +188,7 @@ public void ParseTestMultiValueSeparator(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestNameValueSeparator(ArgumentProviderKind kind) + public void ParseTestNameValueSeparator(ProviderKind kind) { var target = CreateParser(kind); Assert.AreEqual(CommandLineParser.DefaultNameValueSeparator, target.NameValueSeparator); @@ -214,7 +214,7 @@ public void ParseTestNameValueSeparator(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestKeyValueSeparator(ArgumentProviderKind kind) + public void ParseTestKeyValueSeparator(ProviderKind kind) { var target = CreateParser(kind); Assert.AreEqual("=", target.GetArgument("DefaultSeparator")!.KeyValueSeparator); @@ -242,7 +242,7 @@ public void ParseTestKeyValueSeparator(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsage(ArgumentProviderKind kind) + public void TestWriteUsage(ProviderKind kind) { var options = new ParseOptions() { @@ -261,7 +261,7 @@ public void TestWriteUsage(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageLongShort(ArgumentProviderKind kind) + public void TestWriteUsageLongShort(ProviderKind kind) { var target = CreateParser(kind); var options = new UsageWriter() @@ -288,7 +288,7 @@ public void TestWriteUsageLongShort(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageFilter(ArgumentProviderKind kind) + public void TestWriteUsageFilter(ProviderKind kind) { var target = CreateParser(kind); var options = new UsageWriter() @@ -311,7 +311,7 @@ public void TestWriteUsageFilter(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageColor(ArgumentProviderKind kind) + public void TestWriteUsageColor(ProviderKind kind) { var options = new ParseOptions() { @@ -334,7 +334,7 @@ public void TestWriteUsageColor(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageOrder(ArgumentProviderKind kind) + public void TestWriteUsageOrder(ProviderKind kind) { var parser = CreateParser(kind); var options = new UsageWriter() @@ -379,7 +379,7 @@ public void TestWriteUsageOrder(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageSeparator(ArgumentProviderKind kind) + public void TestWriteUsageSeparator(ProviderKind kind) { var options = new ParseOptions() { @@ -397,7 +397,7 @@ public void TestWriteUsageSeparator(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageCustomIndent(ArgumentProviderKind kind) + public void TestWriteUsageCustomIndent(ProviderKind kind) { var options = new ParseOptions() { @@ -414,7 +414,7 @@ public void TestWriteUsageCustomIndent(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestStaticParse(ArgumentProviderKind kind) + public void TestStaticParse(ProviderKind kind) { using var output = new StringWriter(); using var lineWriter = new LineWrappingTextWriter(output, 0); @@ -475,7 +475,7 @@ public void TestStaticParse(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestCancelParsing(ArgumentProviderKind kind) + public void TestCancelParsing(ProviderKind kind) { var parser = CreateParser(kind); @@ -559,7 +559,7 @@ static void handler2(object sender, ArgumentParsedEventArgs e) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestParseOptionsAttribute(ArgumentProviderKind kind) + public void TestParseOptionsAttribute(ProviderKind kind) { var parser = CreateParser(kind); Assert.IsFalse(parser.AllowWhiteSpaceValueSeparator); @@ -602,7 +602,7 @@ public void TestParseOptionsAttribute(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestCulture(ArgumentProviderKind kind) + public void TestCulture(ProviderKind kind) { var result = StaticParse(kind, new[] { "-Argument", "5.5" }); Assert.IsNotNull(result); @@ -624,7 +624,7 @@ public void TestCulture(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestLongShortMode(ArgumentProviderKind kind) + public void TestLongShortMode(ProviderKind kind) { var parser = CreateParser(kind); Assert.AreEqual(ParsingMode.LongShort, parser.Mode); @@ -675,7 +675,7 @@ public void TestLongShortMode(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestMethodArguments(ArgumentProviderKind kind) + public void TestMethodArguments(ProviderKind kind) { var parser = CreateParser(kind); @@ -728,7 +728,7 @@ public void TestMethodArguments(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestAutomaticArgumentConflict(ArgumentProviderKind kind) + public void TestAutomaticArgumentConflict(ProviderKind kind) { CommandLineParser parser = CreateParser(kind); VerifyArgument(parser.GetArgument("Help"), new ExpectedArgument("Help", typeof(int))); @@ -740,7 +740,7 @@ public void TestAutomaticArgumentConflict(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestHiddenArgument(ArgumentProviderKind kind) + public void TestHiddenArgument(ProviderKind kind) { var parser = CreateParser(kind); @@ -760,7 +760,7 @@ public void TestHiddenArgument(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNameTransformPascalCase(ArgumentProviderKind kind) + public void TestNameTransformPascalCase(ProviderKind kind) { var options = new ParseOptions { @@ -781,7 +781,7 @@ public void TestNameTransformPascalCase(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNameTransformCamelCase(ArgumentProviderKind kind) + public void TestNameTransformCamelCase(ProviderKind kind) { var options = new ParseOptions { @@ -802,7 +802,7 @@ public void TestNameTransformCamelCase(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNameTransformSnakeCase(ArgumentProviderKind kind) + public void TestNameTransformSnakeCase(ProviderKind kind) { var options = new ParseOptions { @@ -823,7 +823,7 @@ public void TestNameTransformSnakeCase(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNameTransformDashCase(ArgumentProviderKind kind) + public void TestNameTransformDashCase(ProviderKind kind) { var options = new ParseOptions { @@ -844,7 +844,7 @@ public void TestNameTransformDashCase(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestValueDescriptionTransform(ArgumentProviderKind kind) + public void TestValueDescriptionTransform(ProviderKind kind) { var options = new ParseOptions { @@ -863,7 +863,7 @@ public void TestValueDescriptionTransform(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestValidation(ArgumentProviderKind kind) + public void TestValidation(ProviderKind kind) { // Reset for multiple runs. ValidationArguments.Arg3Value = 0; @@ -926,7 +926,7 @@ public void TestValidation(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestRequires(ArgumentProviderKind kind) + public void TestRequires(ProviderKind kind) { var parser = CreateParser(kind); @@ -947,7 +947,7 @@ public void TestRequires(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestProhibits(ArgumentProviderKind kind) + public void TestProhibits(ProviderKind kind) { var parser = CreateParser(kind); @@ -958,7 +958,7 @@ public void TestProhibits(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestRequiresAny(ArgumentProviderKind kind) + public void TestRequiresAny(ProviderKind kind) { var parser = CreateParser(kind); @@ -968,7 +968,7 @@ public void TestRequiresAny(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestValidatorUsageHelp(ArgumentProviderKind kind) + public void TestValidatorUsageHelp(ProviderKind kind) { CommandLineParser parser = CreateParser(kind); var options = new UsageWriter() @@ -987,7 +987,7 @@ public void TestValidatorUsageHelp(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestDefaultValueDescriptions(ArgumentProviderKind kind) + public void TestDefaultValueDescriptions(ProviderKind kind) { var options = new ParseOptions() { @@ -1006,7 +1006,7 @@ public void TestDefaultValueDescriptions(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestMultiValueWhiteSpaceSeparator(ArgumentProviderKind kind) + public void TestMultiValueWhiteSpaceSeparator(ProviderKind kind) { var parser = CreateParser(kind); Assert.IsTrue(parser.GetArgument("Multi").AllowMultiValueWhiteSpaceSeparator); @@ -1030,7 +1030,7 @@ public void TestMultiValueWhiteSpaceSeparator(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestInjection(ArgumentProviderKind kind) + public void TestInjection(ProviderKind kind) { var parser = CreateParser(kind); var result = parser.Parse(new[] { "-Arg", "1" }); @@ -1048,7 +1048,7 @@ public void TestInjection(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestDuplicateArguments(ArgumentProviderKind kind) + public void TestDuplicateArguments(ProviderKind kind) { var parser = CreateParser(kind); CheckThrows(() => parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }), parser, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1"); @@ -1094,7 +1094,7 @@ public void TestDuplicateArguments(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestConversion(ArgumentProviderKind kind) + public void TestConversion(ProviderKind kind) { var parser = CreateParser(kind); var result = parser.Parse("-ParseCulture 1 -ParseStruct 2 -Ctor 3 -ParseNullable 4 -ParseMulti 5 6 -ParseNullableMulti 7 8 -NullableMulti 9 10 -Nullable 11".Split(' ')); @@ -1122,7 +1122,7 @@ public void TestConversion(ArgumentProviderKind kind) [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestDerivedClass(ArgumentProviderKind kind) + public void TestDerivedClass(ProviderKind kind) { var parser = CreateParser(kind); Assert.AreEqual(4, parser.Arguments.Count); @@ -1273,7 +1273,7 @@ private static void CheckThrows(Action operation, CommandLineParser parser, Comm } } - internal static CommandLineParser CreateParser(ArgumentProviderKind kind, ParseOptions options = null) + internal static CommandLineParser CreateParser(ProviderKind kind, ParseOptions options = null) #if NET7_0_OR_GREATER where T : class, IParserProvider #else @@ -1282,11 +1282,11 @@ internal static CommandLineParser CreateParser(ArgumentProviderKind kind, { var parser = kind switch { - ArgumentProviderKind.Reflection => new CommandLineParser(options), + ProviderKind.Reflection => new CommandLineParser(options), #if NET7_0_OR_GREATER - ArgumentProviderKind.Generated => T.CreateParser(options), + ProviderKind.Generated => T.CreateParser(options), #else - ArgumentProviderKind.Generated => (CommandLineParser)typeof(T).InvokeMember("CreateParser", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { options }), + ProviderKind.Generated => (CommandLineParser)typeof(T).InvokeMember("CreateParser", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { options }), #endif _ => throw new InvalidOperationException() }; @@ -1295,7 +1295,7 @@ internal static CommandLineParser CreateParser(ArgumentProviderKind kind, return parser; } - private static T StaticParse(ArgumentProviderKind kind, string[] args, ParseOptions options = null) + private static T StaticParse(ProviderKind kind, string[] args, ParseOptions options = null) #if NET7_0_OR_GREATER where T : class, IParser #else @@ -1304,11 +1304,11 @@ private static T StaticParse(ArgumentProviderKind kind, string[] args, ParseO { return kind switch { - ArgumentProviderKind.Reflection => CommandLineParser.Parse(args, options), + ProviderKind.Reflection => CommandLineParser.Parse(args, options), #if NET7_0_OR_GREATER - ArgumentProviderKind.Generated => T.Parse(args, options), + ProviderKind.Generated => T.Parse(args, options), #else - ArgumentProviderKind.Generated => (T)typeof(T).InvokeMember("Parse", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { args, options }), + ProviderKind.Generated => (T)typeof(T).InvokeMember("Parse", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { args, options }), #endif _ => throw new InvalidOperationException() }; @@ -1322,8 +1322,8 @@ public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, obje public static IEnumerable ProviderKinds => new[] { - new object[] { ArgumentProviderKind.Reflection }, - new object[] { ArgumentProviderKind.Generated } + new object[] { ProviderKind.Reflection }, + new object[] { ProviderKind.Generated } }; } } diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index d401050f..fc81bb00 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -725,9 +725,9 @@ public IEnumerable Validators /// Gets the kind of provider that was used to determine the available arguments. /// /// - /// One of the values of the enumeration. + /// One of the values of the enumeration. /// - public ArgumentProviderKind ProviderKind => _provider.Kind; + public ProviderKind ProviderKind => _provider.Kind; internal IComparer? ShortArgumentNameComparer => _argumentsByShortName?.Comparer; diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index ed123626..3d57f225 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -48,8 +48,7 @@ namespace Ookii.CommandLine.Commands /// Usage documentation public class CommandManager { - private readonly Assembly? _assembly; - private readonly IEnumerable? _assemblies; + private readonly CommandProvider _provider; private readonly CommandOptions _options; /// @@ -65,6 +64,12 @@ public CommandManager(CommandOptions? options = null) { } + public CommandManager(CommandProvider provider, CommandOptions? options = null) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _options = options ?? new(); + } + /// /// Initializes a new instance of the class. /// @@ -85,9 +90,8 @@ public CommandManager(CommandOptions? options = null) /// /// public CommandManager(Assembly assembly, CommandOptions? options = null) + : this(new ReflectionCommandProvider(assembly ?? throw new ArgumentNullException(nameof(assembly))), options) { - _assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); - _options = options ?? new(); } /// @@ -102,14 +106,8 @@ public CommandManager(Assembly assembly, CommandOptions? options = null) /// or one of its elements is . /// public CommandManager(IEnumerable assemblies, CommandOptions? options = null) + : this(new ReflectionCommandProvider(assemblies ?? throw new ArgumentNullException(nameof(assemblies))), options) { - _assemblies = assemblies ?? throw new ArgumentNullException(nameof(assemblies)); - _options = options ?? new(); - - if (_assemblies.Any(a => a == null)) - { - throw new ArgumentNullException(nameof(assemblies)); - } } /// @@ -168,7 +166,7 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// public IEnumerable GetCommands() { - var commands = GetCommandsUnsorted(); + var commands = _provider.GetCommandsUnsorted(this); if (_options.AutoVersionCommand && !commands.Any(c => _options.CommandNameComparer.Compare(c.Name, Properties.Resources.AutomaticVersionCommandName) == 0)) { @@ -219,12 +217,10 @@ public IEnumerable GetCommands() throw new ArgumentNullException(nameof(commandName)); } - var commands = GetCommandsUnsorted() - .Where(c => c.MatchesName(commandName, _options.CommandNameComparer)); - - if (commands.Any()) + var command = _provider.GetCommand(commandName, this); + if (command != null) { - return commands.First(); + return command; } if (_options.AutoVersionCommand && @@ -540,27 +536,6 @@ public string GetUsage() /// 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 from type in types - let info = CommandInfo.TryCreate(type, this) - where info != null && (_options.CommandFilter?.Invoke(info) ?? true) - select info; - } + public string? GetApplicationDescription() => _provider.GetApplicationDescription(); } } diff --git a/src/Ookii.CommandLine/Commands/CommandProvider.cs b/src/Ookii.CommandLine/Commands/CommandProvider.cs new file mode 100644 index 00000000..e70491bc --- /dev/null +++ b/src/Ookii.CommandLine/Commands/CommandProvider.cs @@ -0,0 +1,15 @@ +using Ookii.CommandLine.Support; +using System.Collections.Generic; + +namespace Ookii.CommandLine.Commands; + +public abstract class CommandProvider +{ + public virtual ProviderKind Kind => ProviderKind.Unknown; + + public abstract IEnumerable GetCommandsUnsorted(CommandManager manager); + + public abstract CommandInfo? GetCommand(string commandName, CommandManager manager); + + public abstract string? GetApplicationDescription(); +} diff --git a/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs b/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs new file mode 100644 index 00000000..0c39ccdf --- /dev/null +++ b/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs @@ -0,0 +1,61 @@ +using Ookii.CommandLine.Support; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace Ookii.CommandLine.Commands; + +internal class ReflectionCommandProvider : CommandProvider +{ + private readonly Assembly? _assembly; + private readonly IEnumerable? _assemblies; + + public ReflectionCommandProvider(Assembly assembly) + { + _assembly = assembly; + } + + public ReflectionCommandProvider(IEnumerable assemblies) + { + _assemblies = assemblies; + if (_assemblies.Any(a => a == null)) + { + throw new ArgumentNullException(nameof(assemblies)); + } + } + + public override ProviderKind Kind => ProviderKind.Reflection; + + public override CommandInfo? GetCommand(string commandName, CommandManager manager) + { + return GetCommandsUnsorted(manager) + .Where(c => c.MatchesName(commandName, manager.Options.CommandNameComparer)) + .FirstOrDefault(); + } + + 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 + let info = CommandInfo.TryCreate(type, manager) + where info != null && (manager.Options.CommandFilter?.Invoke(info) ?? true) + select info; + } + } + + public override string? GetApplicationDescription() + => (_assembly ?? _assemblies?.FirstOrDefault())?.GetCustomAttribute()?.Description; +} diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs index f0dabf9a..618a4640 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -37,9 +37,9 @@ protected ArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, I /// Gets the kind of argument provider. /// /// - /// One of the values of the enumeration. + /// One of the values of the enumeration. /// - public virtual ArgumentProviderKind Kind => ArgumentProviderKind.Unknown; + public virtual ProviderKind Kind => ProviderKind.Unknown; /// /// Gets the type that will hold the argument values. diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs index dedefe3c..0f2f98e6 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -44,7 +44,7 @@ protected GeneratedArgumentProvider(Type argumentsType, ParseOptionsAttribute? o } /// - public override ArgumentProviderKind Kind => ArgumentProviderKind.Generated; + public override ProviderKind Kind => ProviderKind.Generated; /// public override string ApplicationFriendlyName diff --git a/src/Ookii.CommandLine/Support/ArgumentProviderKind.cs b/src/Ookii.CommandLine/Support/ProviderKind.cs similarity index 89% rename from src/Ookii.CommandLine/Support/ArgumentProviderKind.cs rename to src/Ookii.CommandLine/Support/ProviderKind.cs index 55f9c739..3435865b 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProviderKind.cs +++ b/src/Ookii.CommandLine/Support/ProviderKind.cs @@ -3,7 +3,7 @@ /// /// Specifies the kind of provider that was the source of the arguments. /// -public enum ArgumentProviderKind +public enum ProviderKind { /// /// A custom provider that was not part of Ookii.CommandLine. diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs index 2d8bf64e..10248b8a 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -17,7 +17,7 @@ public ReflectionArgumentProvider(Type type) { } - public override ArgumentProviderKind Kind => ArgumentProviderKind.Reflection; + public override ProviderKind Kind => ProviderKind.Reflection; public override string ApplicationFriendlyName { From 7aae26dacbc04512b155c8aa543f64cc51e5d5be Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 20 Apr 2023 18:04:54 -0700 Subject: [PATCH 039/234] Create infrastructure for generated command providers. --- .../Commands/AutomaticVersionCommand.cs | 70 +++++++++++++------ ...Info.cs => AutomaticVersionCommandInfo.cs} | 14 ++-- src/Ookii.CommandLine/Commands/CommandInfo.cs | 16 +---- .../Commands/CommandManager.cs | 20 +++++- .../Commands/CommandProvider.cs | 15 ---- .../Commands/ReflectionCommandInfo.cs | 10 +++ .../Commands/ReflectionCommandProvider.cs | 7 -- .../Support/ArgumentProvider.cs | 4 +- .../Support/CommandProvider.cs | 37 ++++++++++ .../Support/GeneratedArgument.cs | 2 +- .../Support/GeneratedCommandInfo.cs | 63 +++++++++++++++++ .../GeneratedCommandInfoWithCustomParsing.cs | 27 +++++++ src/Samples/TrimTest/Program.cs | 28 ++++++-- 13 files changed, 240 insertions(+), 73 deletions(-) rename src/Ookii.CommandLine/Commands/{BasicCommandInfo.cs => AutomaticVersionCommandInfo.cs} (50%) delete mode 100644 src/Ookii.CommandLine/Commands/CommandProvider.cs create mode 100644 src/Ookii.CommandLine/Support/CommandProvider.cs create mode 100644 src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs create mode 100644 src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs diff --git a/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs b/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs index c19d92eb..9fb906ba 100644 --- a/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs +++ b/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs @@ -1,33 +1,61 @@ -using System; +using Ookii.CommandLine.Support; +using Ookii.CommandLine.Validation; +using System; +using System.Collections.Generic; +using System.ComponentModel; 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) => 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/BasicCommandInfo.cs b/src/Ookii.CommandLine/Commands/AutomaticVersionCommandInfo.cs similarity index 50% rename from src/Ookii.CommandLine/Commands/BasicCommandInfo.cs rename to src/Ookii.CommandLine/Commands/AutomaticVersionCommandInfo.cs index a10d728a..4fe9de07 100644 --- a/src/Ookii.CommandLine/Commands/BasicCommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/AutomaticVersionCommandInfo.cs @@ -6,17 +6,14 @@ namespace Ookii.CommandLine.Commands; -internal class BasicCommandInfo : CommandInfo +internal class AutomaticVersionCommandInfo : CommandInfo { - private readonly string _description; - - public BasicCommandInfo(Type commandType, string name, string description, CommandManager manager) - : base(commandType, name, manager) + public AutomaticVersionCommandInfo(CommandManager manager) + : base(typeof(AutomaticVersionCommand), manager.Options.AutoVersionCommandName(), manager) { - _description = description; } - public override string? Description => _description; + public override string? Description => Manager.Options.StringProvider.AutomaticVersionCommandDescription(); public override bool UseCustomArgumentParsing => false; @@ -24,4 +21,7 @@ public BasicCommandInfo(Type commandType, string name, string description, Comma 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/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index ed7e3860..93325d14 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -211,15 +211,7 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// must use the method. /// /// - public virtual CommandLineParser CreateParser() - { - if (UseCustomArgumentParsing) - { - throw new InvalidOperationException(Properties.Resources.NoParserForCustomParsingCommand); - } - - return new CommandLineParser(CommandType, Manager.Options); - } + public abstract CommandLineParser CreateParser(); /// /// Creates an instance of a command that uses the @@ -314,11 +306,7 @@ public static CommandInfo Create(Type commandType, CommandManager manager) public static bool IsCommand(Type commandType) => ReflectionCommandInfo.GetCommandAttribute(commandType) != null; internal static CommandInfo GetAutomaticVersionCommand(CommandManager manager) - { - var name = manager.Options.AutoVersionCommandName(); - var description = manager.Options.StringProvider.AutomaticVersionCommandDescription(); - return new BasicCommandInfo(typeof(AutomaticVersionCommand), name, description, manager); - } + => new AutomaticVersionCommandInfo(manager); private static string GetName(CommandAttribute attribute, Type commandType, CommandOptions? options) { diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index 3d57f225..550a31cc 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -1,4 +1,5 @@ -using System; +using Ookii.CommandLine.Support; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; @@ -64,6 +65,18 @@ public CommandManager(CommandOptions? options = null) { } + + /// + /// 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. + /// public CommandManager(CommandProvider provider, CommandOptions? options = null) { _provider = provider ?? throw new ArgumentNullException(nameof(provider)); @@ -217,7 +230,10 @@ public IEnumerable GetCommands() throw new ArgumentNullException(nameof(commandName)); } - var command = _provider.GetCommand(commandName, this); + var command = _provider.GetCommandsUnsorted(this) + .Where(c => c.MatchesName(commandName, Options.CommandNameComparer)) + .FirstOrDefault(); + if (command != null) { return command; diff --git a/src/Ookii.CommandLine/Commands/CommandProvider.cs b/src/Ookii.CommandLine/Commands/CommandProvider.cs deleted file mode 100644 index e70491bc..00000000 --- a/src/Ookii.CommandLine/Commands/CommandProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Ookii.CommandLine.Support; -using System.Collections.Generic; - -namespace Ookii.CommandLine.Commands; - -public abstract class CommandProvider -{ - public virtual ProviderKind Kind => ProviderKind.Unknown; - - public abstract IEnumerable GetCommandsUnsorted(CommandManager manager); - - public abstract CommandInfo? GetCommand(string commandName, CommandManager manager); - - public abstract string? GetApplicationDescription(); -} diff --git a/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs index 41f7fb2d..e7a90e5b 100644 --- a/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs @@ -41,6 +41,16 @@ private ReflectionCommandInfo(Type commandType, CommandAttribute attribute, Comm 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) diff --git a/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs b/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs index 0c39ccdf..01f2031c 100644 --- a/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs +++ b/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs @@ -28,13 +28,6 @@ public ReflectionCommandProvider(IEnumerable assemblies) public override ProviderKind Kind => ProviderKind.Reflection; - public override CommandInfo? GetCommand(string commandName, CommandManager manager) - { - return GetCommandsUnsorted(manager) - .Where(c => c.MatchesName(commandName, manager.Options.CommandNameComparer)) - .FirstOrDefault(); - } - public override IEnumerable GetCommandsUnsorted(CommandManager manager) { { diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs index 618a4640..f393d73e 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -10,8 +10,8 @@ namespace Ookii.CommandLine.Support; /// A source of arguments for the . /// /// -/// This interface is used by the source generator when using -/// attribute. It should not normally be used by regular code. +/// This class is used by the source generator when using +/// attribute. It should not normally be used by other code. /// public abstract class ArgumentProvider { diff --git a/src/Ookii.CommandLine/Support/CommandProvider.cs b/src/Ookii.CommandLine/Support/CommandProvider.cs new file mode 100644 index 00000000..3a745b89 --- /dev/null +++ b/src/Ookii.CommandLine/Support/CommandProvider.cs @@ -0,0 +1,37 @@ +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 +/// 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 + /// + /// + public abstract string? GetApplicationDescription(); +} diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 81c24a6e..7f87d24c 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -28,7 +28,7 @@ private GeneratedArgument(ArgumentInfo info, Action? setPropert } /// - /// + /// This class is for internal use by the source generator, and should not be used in your code. /// /// /// diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs new file mode 100644 index 00000000..503bcedc --- /dev/null +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs @@ -0,0 +1,63 @@ +using Ookii.CommandLine.Commands; +using System; +using System.Linq; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Ookii.CommandLine.Support; + +/// +/// This class is for internal use by the source generator, and should not be used in your code. +/// +public class GeneratedCommandInfo : CommandInfo +{ + private readonly DescriptionAttribute? _descriptionAttribute; + private readonly IEnumerable? _aliases; + private readonly Func? _createParser; + + /// + /// This class is for internal use by the source generator, and should not be used in your code. + /// + /// + /// + /// + /// + /// + /// + public GeneratedCommandInfo(CommandManager manager, + Type commandType, + CommandAttribute attribute, + DescriptionAttribute? descriptionAttribute = null, + IEnumerable? aliasAttributes = null, + Func? createParser = null) + : base(commandType, attribute, manager) + { + _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..247aeed8 --- /dev/null +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs @@ -0,0 +1,27 @@ +using Ookii.CommandLine.Commands; +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Ookii.CommandLine.Support; + +/// +public class GeneratedCommandInfoWithCustomParsing : GeneratedCommandInfo + where T : class, ICommandWithCustomParsing, new() +{ + /// + public GeneratedCommandInfoWithCustomParsing(CommandManager manager, + Type commandType, + CommandAttribute attribute, + DescriptionAttribute? descriptionAttribute = null, + IEnumerable? aliasAttributes = null) + : base(manager, commandType, attribute, descriptionAttribute, aliasAttributes) + { + } + + /// + public override bool UseCustomArgumentParsing => true; + + /// + public override ICommandWithCustomParsing CreateInstanceWithCustomParsing() => new T(); +} diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 4e5f2d1a..b8f8e2be 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -1,5 +1,6 @@ // See https://aka.ms/new-console-template for more information using Ookii.CommandLine; +using Ookii.CommandLine.Commands; using Ookii.CommandLine.Conversion; using Ookii.CommandLine.Support; using Ookii.CommandLine.Validation; @@ -7,10 +8,22 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -var arguments = Arguments.Parse(); -if (arguments != null) +var manager = new CommandManager(new MyProvider()); +return manager.RunCommand() ?? 1; + +//var arguments = Arguments.Parse(); +//if (arguments != null) +//{ +// Console.WriteLine($"Hello, World! {arguments.Test}"); +//} + +class MyProvider : CommandProvider { - Console.WriteLine($"Hello, World! {arguments.Test}"); + public override string? GetApplicationDescription() => "Trim Test"; + public override IEnumerable GetCommandsUnsorted(CommandManager manager) + { + yield return new GeneratedCommandInfo(manager, typeof(Arguments), new CommandAttribute(), new DescriptionAttribute("This is a command test"), createParser: options => Arguments.CreateParser(options)); + } } [GeneratedParser] @@ -18,7 +31,8 @@ [Description("This is a test")] [ApplicationFriendlyName("Trim Test")] [RequiresAny(nameof(Test), nameof(Test2))] -partial class Arguments +[Command] +partial class Arguments : ICommand { [CommandLineArgument] [Description("Test argument")] @@ -50,4 +64,10 @@ partial class Arguments public static void Foo(CommandLineParser p) { } + + public int Run() + { + Console.WriteLine("Hello"); + return 0; + } } From c536efe99329688929c3f0f509001b5f1236a685 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 20 Apr 2023 18:15:54 -0700 Subject: [PATCH 040/234] Implement IsCommand in the generator. --- .../Diagnostics.cs | 8 ++++++++ .../ParserGenerator.cs | 19 +++++++++++++++++-- .../Properties/Resources.Designer.cs | 18 ++++++++++++++++++ .../Properties/Resources.resx | 6 ++++++ src/Ookii.CommandLine.Generator/TypeHelper.cs | 4 ++++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 4f4e450c..2e9d68d7 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -105,6 +105,14 @@ public static Diagnostic NonPublicInstanceProperty(ISymbol property) => CreateDi property.ContainingType?.ToDisplayString(), property.Name); + public static Diagnostic CommandAttributeWithoutInterface(INamedTypeSymbol symbol) => CreateDiagnostic( + "CLW0004", + nameof(Resources.CommandAttributeWithoutInterfaceTitle), + nameof(Resources.CommandAttributeWithoutInterfaceMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 0c6a7cfa..67cfe893 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -96,6 +96,22 @@ private void GenerateProvider() } } + var isCommand = false; + if (commandAttribute != null) + { + if (_argumentsClass.ImplementsInterface(_typeHelper.ICommand)) + { + isCommand = true; + } + else + { + // The other way around (interface without attribute) doesn't need a warning since + // it could be a base class for a command (though it's kind of weird that the + // GeneratedParserAttribute was used on a base class). + _context.ReportDiagnostic(Diagnostics.CommandAttributeWithoutInterface(_argumentsClass)); + } + } + _builder.AppendLine("private class GeneratedProvider : Ookii.CommandLine.Support.GeneratedArgumentProvider"); _builder.OpenBlock(); _builder.AppendLine("public GeneratedProvider()"); @@ -116,8 +132,7 @@ private void GenerateProvider() _builder.DecreaseIndent(); _builder.AppendLine("{}"); _builder.AppendLine(); - // TODO: IsCommand - _builder.AppendLine("public override bool IsCommand => false;"); + _builder.AppendLine($"public override bool IsCommand => {isCommand.ToCSharpString()};"); _builder.AppendLine(); if (_argumentsClass.FindConstructor(_typeHelper.CommandLineParser) != null) { diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index e694d9a5..8c943fa1 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -132,6 +132,24 @@ internal static string ArgumentsTypeNotReferenceTypeTitle { } } + /// + /// 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 ICommand.. + /// + internal static string CommandAttributeWithoutInterfaceTitle { + get { + return ResourceManager.GetString("CommandAttributeWithoutInterfaceTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to The multi-value command line argument defined by {0}.{1} must have an array rank of one.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 163c21b6..ad487c75 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -141,6 +141,12 @@ The command line arguments type must be a reference type. + + 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 ICommand. + The multi-value command line argument defined by {0}.{1} must have an array rank of one. diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index 1c9d4cf4..fef72555 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -60,4 +60,8 @@ public TypeHelper(Compilation compilation) public INamedTypeSymbol? ValueConverterAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.ValueConverterAttribute"); + public INamedTypeSymbol? ICommand => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommand"); + + public INamedTypeSymbol? ICommandWithCustomParsing => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommandWithCustomParsing"); + } From 623fa145bfb3de5ce72aab84dd8046a13fb9af1f Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 20 Apr 2023 18:23:28 -0700 Subject: [PATCH 041/234] Improvements to interface detection. --- src/Ookii.CommandLine.Generator/Extensions.cs | 4 +--- src/Ookii.CommandLine.Generator/ParserGenerator.cs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 916defa7..5593fa30 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -45,8 +45,6 @@ public static INamedTypeSymbol GetUnderlyingType(this INamedTypeSymbol type) public static ITypeSymbol GetUnderlyingType(this ITypeSymbol type) => type is INamedTypeSymbol namedType && namedType.IsNullableValueType() ? (INamedTypeSymbol)namedType.TypeArguments[0] : type; - public static bool IsEnum(this ITypeSymbol type) => type.BaseType?.SpecialType == SpecialType.System_Enum; - public static INamedTypeSymbol? FindGenericInterface(this ITypeSymbol type, ITypeSymbol? interfaceToFind) { if (interfaceToFind == null) @@ -83,7 +81,7 @@ public static bool IsConstructedFrom(this INamedTypeSymbol type, ITypeSymbol typ public static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol? interfaceType) { - if (interfaceType == null) + if (interfaceType == null || interfaceType.TypeKind != TypeKind.Interface) { return false; } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 67cfe893..6855e799 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -554,7 +554,7 @@ private static bool CheckAttribute(AttributeData data, ITypeSymbol? attributeTyp return "Ookii.CommandLine.Conversion.BooleanConverter.Instance"; } - if (elementType.IsEnum()) + if (elementType.TypeKind == TypeKind.Enum) { return $"new Ookii.CommandLine.Conversion.EnumConverter(typeof({elementType.ToDisplayString()}))"; } From 41aaf5cb60ebc39fcabf14610e30f0ebefdcce77 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 21 Apr 2023 17:37:01 -0700 Subject: [PATCH 042/234] Support generated command provider. --- .../CommandGenerator.cs | 127 +++++++++++++++ .../ConverterGenerator.cs | 1 + .../ParserGenerator.cs | 20 ++- .../ParserIncrementalGenerator.cs | 34 +++- src/Ookii.CommandLine.Generator/TypeHelper.cs | 10 +- src/Ookii.CommandLine.Tests/CommandTypes.cs | 149 +++++++++--------- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 81 +++++++--- .../Commands/CommandManager.cs | 23 ++- .../GeneratedCommandProviderAttribute.cs | 15 ++ .../Commands/ReflectionCommandProvider.cs | 2 +- .../GeneratedCommandInfoWithCustomParsing.cs | 3 +- src/Samples/TrimTest/Program.cs | 5 +- 12 files changed, 357 insertions(+), 113 deletions(-) create mode 100644 src/Ookii.CommandLine.Generator/CommandGenerator.cs create mode 100644 src/Ookii.CommandLine/Commands/GeneratedCommandProviderAttribute.cs diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs new file mode 100644 index 00000000..902784a2 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -0,0 +1,127 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using System.Text; + +namespace Ookii.CommandLine.Generator; + +internal class CommandGenerator +{ + private readonly TypeHelper _typeHelper; + private readonly SourceProductionContext _context; + private readonly List<(INamedTypeSymbol Type, AttributeData CommandAttribute, AttributeData? DescriptionAttribute, + List? AliasAttributes)> _commands = new(); + + private readonly List _providers = new(); + + public CommandGenerator(TypeHelper typeHelper, SourceProductionContext context) + { + _typeHelper = typeHelper; + _context = context; + } + + public void AddCommand(INamedTypeSymbol type, AttributeData commandAttribute, AttributeData? descriptionAttribute, List? aliasAttributes) + { + _commands.Add((type, commandAttribute, descriptionAttribute, aliasAttributes)); + } + + public void AddProvider(INamedTypeSymbol provider) + { + _providers.Add(provider); + } + + public void Generate() + { + foreach (var provider in _providers) + { + var source = GenerateProvider(provider); + if (source != null) + { + _context.AddSource(provider.ToDisplayString().ToIdentifier(".g.cs"), SourceText.From(source, Encoding.UTF8)); + } + } + } + + private string? GenerateProvider(INamedTypeSymbol provider) + { + AttributeData? descriptionAttribute = null; + foreach (var attribute in provider.ContainingAssembly.GetAttributes()) + { + if (attribute.AttributeClass?.DerivesFrom(_typeHelper.AssemblyDescriptionAttribute) ?? false) + { + descriptionAttribute = attribute; + break; + } + } + + var builder = new SourceBuilder(provider.ContainingNamespace); + builder.AppendLine($"partial class {provider.Name} : Ookii.CommandLine.Support.CommandProvider"); + builder.OpenBlock(); + builder.AppendLine("public override Ookii.CommandLine.Support.ProviderKind Kind => Ookii.CommandLine.Support.ProviderKind.Generated;"); + builder.AppendLine(); + builder.AppendLine("public override string? GetApplicationDescription()"); + if (descriptionAttribute != null) + { + builder.AppendLine($" => ({descriptionAttribute.CreateInstantiation()}).Description;"); + } + else + { + builder.AppendLine(" => null;"); + } + + builder.AppendLine(); + builder.AppendLine("public override System.Collections.Generic.IEnumerable GetCommandsUnsorted(Ookii.CommandLine.Commands.CommandManager manager)"); + builder.OpenBlock(); + + // TODO: Providers with custom command lists. + foreach (var command in _commands) + { + var useCustomParsing = command.Type.ImplementsInterface(_typeHelper.ICommandWithCustomParsing); + var commandTypeName = command.Type.ToDisplayString(); + if (useCustomParsing) + { + builder.AppendLine($"yield return new Ookii.CommandLine.Support.GeneratedCommandInfoWithCustomParsing<{commandTypeName}>("); + } + else + { + builder.AppendLine("yield return new Ookii.CommandLine.Support.GeneratedCommandInfo("); + } + + builder.IncreaseIndent(); + builder.AppendLine("manager"); + if (!useCustomParsing) + { + builder.AppendLine($", typeof({commandTypeName})"); + } + + builder.AppendLine($", {command.CommandAttribute.CreateInstantiation()}"); + if (command.DescriptionAttribute != null) + { + builder.AppendLine($", descriptionAttribute: {command.DescriptionAttribute.CreateInstantiation()}"); + } + + if (command.AliasAttributes != null) + { + builder.AppendLine($", aliasAttributes: new Ookii.CommandLine.AliasAttribute[] {{ {string.Join(", ", command.AliasAttributes.Select(a => a.CreateInstantiation()))} }}"); + } + + if (!useCustomParsing) + { + builder.AppendLine($", createParser: options => {commandTypeName}.CreateParser(options)"); + } + + builder.DecreaseIndent(); + builder.AppendLine(");"); + } + + // Makes sure the function compiles if there are no commands. + builder.AppendLine("yield break;"); + builder.CloseBlock(); // GetCommandsUnsorted + builder.AppendLine(); + + // TODO: Make optional. + builder.AppendLine("public static Ookii.CommandLine.Commands.CommandManager CreateCommandManager(Ookii.CommandLine.Commands.CommandOptions? options = null)"); + builder.AppendLine($" => new Ookii.CommandLine.Commands.CommandManager(new {provider.ToDisplayString()}(), options);"); + builder.CloseBlock(); // class + return builder.GetSource(); + } +} diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 97bce78e..529030b7 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -45,6 +45,7 @@ public bool IsBetter(ConverterInfo other) // TODO: Customizable or random namespace? private const string GeneratedNamespace = "Ookii.CommandLine.Conversion.Generated"; private const string ConverterSuffix = "Converter"; + // TODO: Use typehelper and/or specialtype. private readonly INamedTypeSymbol? _readOnlySpanType; private readonly INamedTypeSymbol? _stringType; private readonly INamedTypeSymbol? _cultureType; diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 6855e799..09944054 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -26,20 +26,22 @@ private struct MethodArgumentInfo private readonly INamedTypeSymbol _argumentsClass; private readonly SourceBuilder _builder; private readonly ConverterGenerator _converterGenerator; + private readonly CommandGenerator _commandGenerator; - public ParserGenerator(Compilation compilation, SourceProductionContext context, INamedTypeSymbol argumentsClass, ConverterGenerator converterGenerator) + public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumentsClass, TypeHelper typeHelper, ConverterGenerator converterGenerator, CommandGenerator commandGenerator) { - _typeHelper = new TypeHelper(compilation); - _compilation = compilation; + _typeHelper = typeHelper; + _compilation = typeHelper.Compilation; _context = context; _argumentsClass = argumentsClass; _builder = new(argumentsClass.ContainingNamespace); _converterGenerator = converterGenerator; + _commandGenerator = commandGenerator; } - public static string? Generate(Compilation compilation, SourceProductionContext context, INamedTypeSymbol argumentsClass, ConverterGenerator converterGenerator) + public static string? Generate(SourceProductionContext context, INamedTypeSymbol argumentsClass, TypeHelper typeHelper, ConverterGenerator converterGenerator, CommandGenerator commandGenerator) { - var generator = new ParserGenerator(compilation, context, argumentsClass, converterGenerator); + var generator = new ParserGenerator(context, argumentsClass, typeHelper, converterGenerator, commandGenerator); return generator.Generate(); } @@ -79,13 +81,15 @@ private void GenerateProvider() AttributeData? applicationFriendlyName = null; AttributeData? commandAttribute = null; List? classValidators = null; + List? aliasAttributes = null; // for command usage. foreach (var attribute in _argumentsClass.GetAttributes()) { if (CheckAttribute(attribute, _typeHelper.ParseOptionsAttribute, ref parseOptions) || CheckAttribute(attribute, _typeHelper.DescriptionAttribute, ref description) || CheckAttribute(attribute, _typeHelper.ApplicationFriendlyNameAttribute, ref applicationFriendlyName) || CheckAttribute(attribute, _typeHelper.CommandAttribute, ref commandAttribute) || - CheckAttribute(attribute, _typeHelper.ClassValidationAttribute, ref classValidators)) + CheckAttribute(attribute, _typeHelper.ClassValidationAttribute, ref classValidators) || + CheckAttribute(attribute, _typeHelper.AliasAttribute, ref aliasAttributes)) { continue; } @@ -96,12 +100,14 @@ private void GenerateProvider() } } + // TODO: Warn if AliasAttribute without CommandAttribute. var isCommand = false; if (commandAttribute != null) { if (_argumentsClass.ImplementsInterface(_typeHelper.ICommand)) { isCommand = true; + _commandGenerator.AddCommand(_argumentsClass, commandAttribute, description, aliasAttributes); } else { @@ -432,7 +438,7 @@ private void GenerateArgument(ISymbol member) } _builder.DecreaseIndent(); - _builder.AppendLine($");"); + _builder.AppendLine(");"); } // Using a ref parameter with bool return allows me to chain these together. diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index 416e3cbb..85989f74 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -11,6 +11,8 @@ namespace Ookii.CommandLine.Generator; [Generator] public class ParserIncrementalGenerator : IIncrementalGenerator { + private record struct ClassInfo(ClassDeclarationSyntax Syntax, bool IsCommandProvider); + public void Initialize(IncrementalGeneratorInitializationContext context) { var classDeclarations = context.SyntaxProvider @@ -25,16 +27,20 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Left, source.Right!, spc)); } - private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext context) + private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext context) { if (classes.IsDefaultOrEmpty) { return; } + var typeHelper = new TypeHelper(compilation); var converterGenerator = new ConverterGenerator(compilation); - foreach (var syntax in classes) + var commandGenerator = new CommandGenerator(typeHelper, context); + foreach (var cls in classes) { + var info = cls!.Value; + var syntax = info.Syntax; context.CancellationToken.ThrowIfCancellationRequested(); var semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree); if (semanticModel.GetDeclaredSymbol(syntax, context.CancellationToken) is not INamedTypeSymbol symbol) @@ -42,6 +48,7 @@ private static void Execute(Compilation compilation, ImmutableArray _compilation; + public INamedTypeSymbol? Boolean => _compilation.GetSpecialType(SpecialType.System_Boolean); public INamedTypeSymbol? Dictionary => _compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName); @@ -20,7 +24,9 @@ public TypeHelper(Compilation compilation) public INamedTypeSymbol? ICollection => _compilation.GetTypeByMetadataName(typeof(ICollection<>).FullName); - public INamedTypeSymbol? DescriptionAttribute => _compilation.GetTypeByMetadataName("System.ComponentModel.DescriptionAttribute"); + public INamedTypeSymbol? DescriptionAttribute => _compilation.GetTypeByMetadataName(typeof(DescriptionAttribute).FullName); + + public INamedTypeSymbol? AssemblyDescriptionAttribute => _compilation.GetTypeByMetadataName(typeof(AssemblyDescriptionAttribute).FullName); public INamedTypeSymbol? ISpanParsable => _compilation.GetTypeByMetadataName("System.ISpanParsable`1"); @@ -32,6 +38,8 @@ public TypeHelper(Compilation compilation) public INamedTypeSymbol? GeneratedParserAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "GeneratedParserAttribute"); + public INamedTypeSymbol? GeneratedCommandProviderAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.GeneratedCommandProviderAttribute"); + public INamedTypeSymbol? CommandLineArgumentAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "CommandLineArgumentAttribute"); public INamedTypeSymbol? ParseOptionsAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "ParseOptionsAttribute"); diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index 8247cc6b..a512e704 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -3,99 +3,106 @@ using System.ComponentModel; using System.Threading.Tasks; -namespace Ookii.CommandLine.Tests +namespace Ookii.CommandLine.Tests; + +[GeneratedCommandProvider] +partial class GeneratedProvider { } + +[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 - { - [CommandLineArgument] - [Description("Argument description")] - public int Value { get; set; } +[GeneratedParser] +[Command] +[Alias("alias")] +public partial class AnotherSimpleCommand : ICommand +{ + [CommandLineArgument] + [Description("Argument description")] + public int Value { get; set; } - public int Run() - { - return Value; - } + public int Run() + { + return Value; } +} - [Command("custom")] - [Description("Custom parsing command.")] - internal class CustomParsingCommand : ICommandWithCustomParsing +[GeneratedParser] +[Command("custom")] +[Description("Custom parsing command.")] +partial class CustomParsingCommand : ICommandWithCustomParsing +{ + public void Parse(string[] args, int index, CommandOptions options) { - public void Parse(string[] args, int index, CommandOptions options) - { - Value = args[index]; - } + Value = args[index]; + } - 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 - { - [CommandLineArgument(Position = 0)] - [Description("Argument description.")] - public int Value { get; set; } +// Hidden so I don't have to update the expected usage. +[GeneratedParser] +[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() - { - // Do somehting different than RunAsync so the test can differentiate which one was - // called. - return Value + 1; - } + 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); - } + public Task RunAsync() + { + return Task.FromResult(Value); } +} - // Used in stand-alone test, so not an actual command. - class AsyncBaseCommand : AsyncCommandBase +// Used in stand-alone test, so not an actual command. +class AsyncBaseCommand : AsyncCommandBase +{ + public override async Task RunAsync() { - public override async Task RunAsync() - { - // Do something actually async to test the wait in Run(). - await Task.Yield(); - return 42; - } + // Do something actually async to test the wait in Run(). + await Task.Yield(); + return 42; } +} - public class NotACommand : ICommand +public class NotACommand : ICommand +{ + public int Run() { - public int Run() - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 64cdeeb0..38299317 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -1,7 +1,9 @@ // Copyright (c) Sven Groot (Ookii.org) using Microsoft.VisualStudio.TestTools.UnitTesting; using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Support; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -15,9 +17,10 @@ public class SubCommandTest private static readonly Assembly _commandAssembly = Assembly.GetExecutingAssembly(); [TestMethod] - public void GetCommandsTest() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void GetCommandsTest(ProviderKind kind) { - var manager = new CommandManager(_commandAssembly); + var manager = CreateManager(kind); var commands = manager.GetCommands().ToArray(); Assert.IsNotNull(commands); @@ -33,9 +36,10 @@ public void GetCommandsTest() } [TestMethod] - public void GetCommandTest() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void GetCommandTest(ProviderKind kind) { - var manager = new CommandManager(_commandAssembly); + var manager = CreateManager(kind); var command = manager.GetCommand("test"); Assert.IsNotNull(command); Assert.AreEqual("test", command.Name); @@ -75,7 +79,8 @@ public void IsCommandTest() } [TestMethod] - public void CreateCommandTest() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void CreateCommandTest(ProviderKind kind) { using var writer = LineWrappingTextWriter.ForStringWriter(0); var options = new CommandOptions() @@ -87,7 +92,7 @@ public void CreateCommandTest() } }; - var manager = new CommandManager(_commandAssembly, options); + var manager = CreateManager(kind, options); TestCommand command = (TestCommand)manager.CreateCommand("test", new[] { "-Argument", "Foo" }, 0); Assert.IsNotNull(command); Assert.AreEqual(ParseStatus.Success, manager.ParseResult.Status); @@ -135,7 +140,8 @@ public void CreateCommandTest() } [TestMethod] - public void TestWriteUsage() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsage(ProviderKind kind) { using var writer = LineWrappingTextWriter.ForStringWriter(0); var options = new CommandOptions() @@ -147,13 +153,14 @@ public void TestWriteUsage() } }; - var manager = new CommandManager(_commandAssembly, options); + var manager = CreateManager(kind, options); manager.WriteUsage(); Assert.AreEqual(_expectedUsage, writer.BaseWriter.ToString()); } [TestMethod] - public void TestWriteUsageColor() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageColor(ProviderKind kind) { using var writer = LineWrappingTextWriter.ForStringWriter(0); var options = new CommandOptions() @@ -165,13 +172,14 @@ public void TestWriteUsageColor() } }; - var manager = new CommandManager(_commandAssembly, options); + var manager = CreateManager(kind, options); manager.WriteUsage(); Assert.AreEqual(_expectedUsageColor, writer.BaseWriter.ToString()); } [TestMethod] - public void TestWriteUsageInstruction() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageInstruction(ProviderKind kind) { using var writer = LineWrappingTextWriter.ForStringWriter(0); var options = new CommandOptions() @@ -184,13 +192,14 @@ public void TestWriteUsageInstruction() } }; - var manager = new CommandManager(_commandAssembly, options); + var manager = CreateManager(kind, options); manager.WriteUsage(); Assert.AreEqual(_expectedUsageInstruction, writer.BaseWriter.ToString()); } [TestMethod] - public void TestWriteUsageApplicationDescription() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageApplicationDescription(ProviderKind kind) { using var writer = LineWrappingTextWriter.ForStringWriter(0); var options = new CommandOptions() @@ -203,13 +212,14 @@ public void TestWriteUsageApplicationDescription() } }; - var manager = new CommandManager(_commandAssembly, options); + var manager = CreateManager(kind, options); manager.WriteUsage(); Assert.AreEqual(_expectedUsageWithDescription, writer.BaseWriter.ToString()); } [TestMethod] - public void TestCommandUsage() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCommandUsage(ProviderKind kind) { using var writer = LineWrappingTextWriter.ForStringWriter(0); var options = new CommandOptions() @@ -222,7 +232,7 @@ public void TestCommandUsage() }; // This tests whether the command name is included in the help for the command. - var manager = new CommandManager(_commandAssembly, options); + var manager = CreateManager(kind, options); var result = manager.CreateCommand(new[] { "AsyncCommand", "-Help" }); Assert.IsNull(result); Assert.AreEqual(ParseStatus.Canceled, manager.ParseResult.Status); @@ -231,14 +241,15 @@ public void TestCommandUsage() } [TestMethod] - public void TestCommandNameTransform() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCommandNameTransform(ProviderKind kind) { var options = new CommandOptions() { CommandNameTransform = NameTransform.PascalCase }; - var manager = new CommandManager(_commandAssembly, options); + var manager = CreateManager(kind, options); var info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); Assert.AreEqual("AnotherSimple", info.Name); @@ -267,14 +278,15 @@ public void TestCommandNameTransform() } [TestMethod] - public void TestCommandFilter() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCommandFilter(ProviderKind kind) { var options = new CommandOptions() { CommandFilter = cmd => !cmd.UseCustomArgumentParsing, }; - var manager = new CommandManager(_commandAssembly, options); + var manager = CreateManager(kind, options); Assert.IsNull(manager.GetCommand("custom")); Assert.IsNotNull(manager.GetCommand("test")); Assert.IsNotNull(manager.GetCommand("AnotherSimpleCommand")); @@ -282,9 +294,10 @@ public void TestCommandFilter() } [TestMethod] - public async Task TestAsyncCommand() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public async Task TestAsyncCommand(ProviderKind kind) { - var manager = new CommandManager(_commandAssembly); + var manager = CreateManager(kind); var result = await manager.RunCommandAsync(new[] { "AsyncCommand", "5" }); Assert.AreEqual(5, result); @@ -321,6 +334,30 @@ private static void VerifyCommand(CommandInfo command, string name, Type type, b CollectionAssert.AreEqual(aliases ?? Array.Empty(), command.Aliases.ToArray()); } + public static CommandManager CreateManager(ProviderKind kind, CommandOptions options = null) + { + var manager = kind switch + { + ProviderKind.Reflection => new CommandManager(_commandAssembly, options), + ProviderKind.Generated => GeneratedProvider.CreateCommandManager(options), + _ => throw new InvalidOperationException() + }; + + Assert.AreEqual(kind, manager.ProviderKind); + return manager; + } + + 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 } + }; + #region Expected usage private const string _executableName = "test"; diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index 550a31cc..e9fd58c8 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -161,6 +161,14 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// 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 information about the commands. /// @@ -179,7 +187,7 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// public IEnumerable GetCommands() { - var commands = _provider.GetCommandsUnsorted(this); + var commands = GetCommandsUnsortedAndFiltered(); if (_options.AutoVersionCommand && !commands.Any(c => _options.CommandNameComparer.Compare(c.Name, Properties.Resources.AutomaticVersionCommandName) == 0)) { @@ -230,7 +238,7 @@ public IEnumerable GetCommands() throw new ArgumentNullException(nameof(commandName)); } - var command = _provider.GetCommandsUnsorted(this) + var command = GetCommandsUnsortedAndFiltered() .Where(c => c.MatchesName(commandName, Options.CommandNameComparer)) .FirstOrDefault(); @@ -553,5 +561,16 @@ public string GetUsage() /// used by this instance. /// public string? GetApplicationDescription() => _provider.GetApplicationDescription(); + + private IEnumerable GetCommandsUnsortedAndFiltered() + { + var commands = _provider.GetCommandsUnsorted(this); + if (_options.CommandFilter != null) + { + commands = commands.Where(c => _options.CommandFilter(c)); + } + + return commands; + } } } diff --git a/src/Ookii.CommandLine/Commands/GeneratedCommandProviderAttribute.cs b/src/Ookii.CommandLine/Commands/GeneratedCommandProviderAttribute.cs new file mode 100644 index 00000000..b59d13de --- /dev/null +++ b/src/Ookii.CommandLine/Commands/GeneratedCommandProviderAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Ookii.CommandLine.Commands; + +/// +/// Indicates that the class with this attribute uses code generation to provide commands to a +/// class. +/// +/// +/// TODO: Better docs. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class GeneratedCommandProviderAttribute : Attribute +{ +} diff --git a/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs b/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs index 01f2031c..653aeb3b 100644 --- a/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs +++ b/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs @@ -44,7 +44,7 @@ public override IEnumerable GetCommandsUnsorted(CommandManager mana return from type in types let info = CommandInfo.TryCreate(type, manager) - where info != null && (manager.Options.CommandFilter?.Invoke(info) ?? true) + where info != null select info; } } diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs index 247aeed8..e61bc2e5 100644 --- a/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs @@ -11,11 +11,10 @@ public class GeneratedCommandInfoWithCustomParsing : GeneratedCommandInfo { /// public GeneratedCommandInfoWithCustomParsing(CommandManager manager, - Type commandType, CommandAttribute attribute, DescriptionAttribute? descriptionAttribute = null, IEnumerable? aliasAttributes = null) - : base(manager, commandType, attribute, descriptionAttribute, aliasAttributes) + : base(manager, typeof(T), attribute, descriptionAttribute, aliasAttributes) { } diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index b8f8e2be..49180be5 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -8,7 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -var manager = new CommandManager(new MyProvider()); +var manager = TestProvider.CreateCommandManager(); return manager.RunCommand() ?? 1; //var arguments = Arguments.Parse(); @@ -26,6 +26,9 @@ public override IEnumerable GetCommandsUnsorted(CommandManager mana } } +[GeneratedCommandProvider] +partial class TestProvider { } + [GeneratedParser] [ParseOptions(CaseSensitive = true)] [Description("This is a test")] From c046cad2b332dfec7699db36addf91d16e229131 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 21 Apr 2023 17:48:38 -0700 Subject: [PATCH 043/234] Use TypeHelper in ConverterGenerator. --- .../ConverterGenerator.cs | 21 +++++++------------ src/Ookii.CommandLine.Generator/Extensions.cs | 2 +- .../ParserIncrementalGenerator.cs | 2 +- src/Ookii.CommandLine.Generator/TypeHelper.cs | 11 +++++++++- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 529030b7..6fe79906 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -45,21 +45,14 @@ public bool IsBetter(ConverterInfo other) // TODO: Customizable or random namespace? private const string GeneratedNamespace = "Ookii.CommandLine.Conversion.Generated"; private const string ConverterSuffix = "Converter"; - // TODO: Use typehelper and/or specialtype. private readonly INamedTypeSymbol? _readOnlySpanType; - private readonly INamedTypeSymbol? _stringType; private readonly INamedTypeSymbol? _cultureType; private readonly Dictionary _converters = new(SymbolEqualityComparer.Default); - public ConverterGenerator(Compilation compilation) + public ConverterGenerator(TypeHelper typeHelper) { - _stringType = compilation.GetTypeByMetadataName("System.String"); - _cultureType = compilation.GetTypeByMetadataName("System.Globalization.CultureInfo"); - var charType = compilation.GetTypeByMetadataName("System.Char"); - if (charType != null) - { - _readOnlySpanType = compilation.GetTypeByMetadataName("System.ReadOnlySpan`1")?.Construct(charType); - } + _cultureType = typeHelper.CultureInfo; + _readOnlySpanType = typeHelper.ReadOnlySpanOfChar; } public string? GetConverter(ITypeSymbol type) @@ -112,14 +105,14 @@ public ConverterGenerator(Compilation compilation) } var newInfo = new ConverterInfo(); - if (SymbolEqualityComparer.Default.Equals(_readOnlySpanType, ctor.Parameters[0].Type)) + if (ctor.Parameters[0].Type.SymbolEquals(_readOnlySpanType)) { newInfo.UseSpan = true; info = newInfo; // Won't find a better one break; } - else if (!SymbolEqualityComparer.Default.Equals(_stringType, ctor.Parameters[0].Type)) + else if (ctor.Parameters[0].Type.SpecialType != SpecialType.System_String) { continue; } @@ -142,11 +135,11 @@ public ConverterGenerator(Compilation compilation) } var newInfo = new ConverterInfo() { ParseMethod = true }; - if (SymbolEqualityComparer.Default.Equals(_readOnlySpanType, method.Parameters[0].Type)) + if (method.Parameters[0].Type.SymbolEquals(_readOnlySpanType)) { newInfo.UseSpan = true; } - else if (!SymbolEqualityComparer.Default.Equals(_stringType, method.Parameters[0].Type)) + else if (method.Parameters[0].Type.SpecialType != SpecialType.System_String) { continue; } diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 5593fa30..792f4068 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -103,7 +103,7 @@ public static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol? inter } public static bool CanAssignFrom(this ITypeSymbol targetType, ITypeSymbol sourceType) - => SymbolEqualityComparer.Default.Equals(targetType, sourceType) || sourceType.DerivesFrom(targetType) + => targetType.SymbolEquals(sourceType) || sourceType.DerivesFrom(targetType) || sourceType.ImplementsInterface(targetType); public static string CreateInstantiation(this AttributeData attribute) diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index 85989f74..b1a3c259 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -35,7 +35,7 @@ private static void Execute(Compilation compilation, ImmutableArray } var typeHelper = new TypeHelper(compilation); - var converterGenerator = new ConverterGenerator(compilation); + var converterGenerator = new ConverterGenerator(typeHelper); var commandGenerator = new CommandGenerator(typeHelper, context); foreach (var cls in classes) { diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index f717da17..d7c56d63 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using System.ComponentModel; +using System.Globalization; using System.Reflection; namespace Ookii.CommandLine.Generator; @@ -16,7 +17,9 @@ public TypeHelper(Compilation compilation) public Compilation Compilation => _compilation; - public INamedTypeSymbol? Boolean => _compilation.GetSpecialType(SpecialType.System_Boolean); + public INamedTypeSymbol Boolean => _compilation.GetSpecialType(SpecialType.System_Boolean); + + public INamedTypeSymbol Char => _compilation.GetSpecialType(SpecialType.System_Char); public INamedTypeSymbol? Dictionary => _compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName); @@ -32,6 +35,12 @@ public TypeHelper(Compilation compilation) 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"); From fdcab5b034b2c99eba89fba2ec9ed2553f7d0b5d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 24 Apr 2023 18:12:51 -0700 Subject: [PATCH 044/234] Add trimming annotations. --- src/Ookii.CommandLine/CommandLineParser.cs | 23 ++++++++---- .../CommandLineParserGeneric.cs | 4 ++ src/Ookii.CommandLine/Commands/CommandInfo.cs | 12 +++++- .../Commands/CommandManager.cs | 10 +++++ .../Commands/ReflectionCommandInfo.cs | 14 +++---- .../Commands/ReflectionCommandProvider.cs | 4 ++ .../Conversion/ConstructorConverter.cs | 11 +++++- .../Conversion/KeyValuePairConverter.cs | 4 ++ .../Ookii.CommandLine.csproj | 1 + .../Support/ReflectionArgument.cs | 4 ++ .../Support/ReflectionArgumentProvider.cs | 4 ++ src/Ookii.CommandLine/TypeHelper.cs | 37 ++++++++++++++++--- 12 files changed, 105 insertions(+), 23 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index fc81bb00..628e9f57 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -302,6 +302,9 @@ private struct PrefixInfo /// class and are not used here. /// /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] +#endif public CommandLineParser(Type argumentsType, ParseOptions? options = null) : this(new ReflectionArgumentProvider(argumentsType ?? throw new ArgumentNullException(nameof(argumentsType))), options) { @@ -1022,6 +1025,9 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// method. /// /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] +#endif public static T? Parse(ParseOptions? options = null) where T : class { @@ -1055,10 +1061,15 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] +#endif public static T? Parse(string[] args, int index, ParseOptions? options = null) where T : class { - return (T?)ParseInternal(typeof(T), args, index, options); + options ??= new(); + var parser = new CommandLineParser(typeof(T), options); + return (T?)parser.ParseWithErrorHandling(args, index); } /// @@ -1082,6 +1093,9 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] +#endif public static T? Parse(string[] args, ParseOptions? options = null) where T : class { @@ -1198,13 +1212,6 @@ protected virtual void OnDuplicateArgument(DuplicateArgumentEventArgs e) DuplicateArgument?.Invoke(this, e); } - internal static object? ParseInternal(Type argumentsType, string[] args, int index, ParseOptions? options) - { - options ??= new(); - var parser = new CommandLineParser(argumentsType, options); - return parser.ParseWithErrorHandling(args, index); - } - internal static bool ShouldIndent(LineWrappingTextWriter writer) { return writer.MaximumLineLength is 0 or >= 30; diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index 46e37a93..e1f2c1a1 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -1,5 +1,6 @@ using Ookii.CommandLine.Support; using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Ookii.CommandLine @@ -39,6 +40,9 @@ public class CommandLineParser : CommandLineParser /// /// /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] +#endif public CommandLineParser(ParseOptions? options = null) : base(typeof(T), options) { diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index 93325d14..09c12eb0 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -269,6 +270,9 @@ public bool MatchesName(string name, IComparer? comparer = null) /// A class with information about the command, or /// if was not a command. /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandProviderAttribute instead.")] +#endif public static CommandInfo? TryCreate(Type commandType, CommandManager manager) => ReflectionCommandInfo.TryCreate(commandType, manager); @@ -289,8 +293,11 @@ public bool MatchesName(string name, IComparer? comparer = null) /// /// A class with information about the command. /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandProviderAttribute instead.")] +#endif public static CommandInfo Create(Type commandType, CommandManager manager) - => new ReflectionCommandInfo(commandType, manager); + => new ReflectionCommandInfo(commandType, null, manager); /// /// Returns a value indicating if the specified type is a subcommand. @@ -303,6 +310,9 @@ public static CommandInfo Create(Type commandType, CommandManager manager) /// /// is . /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandProviderAttribute instead.")] +#endif public static bool IsCommand(Type commandType) => ReflectionCommandInfo.GetCommandAttribute(commandType) != null; internal static CommandInfo GetAutomaticVersionCommand(CommandManager manager) diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index e9fd58c8..ff578ea6 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -60,6 +61,9 @@ public class CommandManager /// The options to use for parsing and usage help, or to use /// the default options. /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] +#endif public CommandManager(CommandOptions? options = null) : this(Assembly.GetCallingAssembly(), options) { @@ -102,6 +106,9 @@ public CommandManager(CommandProvider provider, CommandOptions? options = null) /// instance to create multiple commands. /// /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandProviderAttribute instead.")] +#endif public CommandManager(Assembly assembly, CommandOptions? options = null) : this(new ReflectionCommandProvider(assembly ?? throw new ArgumentNullException(nameof(assembly))), options) { @@ -118,6 +125,9 @@ public CommandManager(Assembly assembly, CommandOptions? options = null) /// /// or one of its elements is . /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandProviderAttribute instead.")] +#endif public CommandManager(IEnumerable assemblies, CommandOptions? options = null) : this(new ReflectionCommandProvider(assemblies ?? throw new ArgumentNullException(nameof(assemblies))), options) { diff --git a/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs index e7a90e5b..0b1d9044 100644 --- a/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -10,17 +11,16 @@ namespace Ookii.CommandLine.Commands; + +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandProviderAttribute instead.")] +#endif internal class ReflectionCommandInfo : CommandInfo { private string? _description; - public ReflectionCommandInfo(Type commandType, CommandManager manager) - : base(commandType, GetCommandAttributeOrThrow(commandType), manager) - { - } - - private ReflectionCommandInfo(Type commandType, CommandAttribute attribute, CommandManager manager) - : base(commandType, attribute, manager) + public ReflectionCommandInfo(Type commandType, CommandAttribute? attribute, CommandManager manager) + : base(commandType, attribute ?? GetCommandAttributeOrThrow(commandType), manager) { } diff --git a/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs b/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs index 653aeb3b..de98ecf1 100644 --- a/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs +++ b/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs @@ -2,11 +2,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; namespace Ookii.CommandLine.Commands; +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandProviderAttribute instead.")] +#endif internal class ReflectionCommandProvider : CommandProvider { private readonly Assembly? _assembly; diff --git a/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs index 482cd9cd..cf918c9a 100644 --- a/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs @@ -1,13 +1,22 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Ookii.CommandLine.Conversion; internal class ConstructorConverter : ArgumentConverter { +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + private readonly Type _type; - public ConstructorConverter(Type type) + public ConstructorConverter( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + Type type) { _type = type; } diff --git a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs index 90356550..4f5c3ce2 100644 --- a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs +++ b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs @@ -1,6 +1,7 @@ // Copyright (c) Sven Groot (Ookii.org) using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Ookii.CommandLine.Conversion; @@ -71,6 +72,9 @@ public KeyValuePairConverter(ArgumentConverter keyConverter, ArgumentConverter v /// /// Initializes a new instance of the class. /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining converter types via reflection.")] +#endif public KeyValuePairConverter() : this(typeof(TKey).GetStringConverter(null), typeof(TValue).GetStringConverter(null), null, true) { diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index 8dbb3074..113463a6 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -6,6 +6,7 @@ 11.0 True True + True MIT https://github.com/SvenGroot/ookii.commandline https://github.com/SvenGroot/ookii.commandline diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index 4ae7cc25..3928e153 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -8,9 +8,13 @@ using System.Text; using Ookii.CommandLine.Validation; using System.Threading; +using System.Diagnostics.CodeAnalysis; namespace Ookii.CommandLine.Support; +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] +#endif internal class ReflectionArgument : CommandLineArgument { #region Nested types diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs index 10248b8a..7ec68ab5 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text; @@ -10,6 +11,9 @@ namespace Ookii.CommandLine.Support; +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] +#endif internal class ReflectionArgumentProvider : ArgumentProvider { public ReflectionArgumentProvider(Type type) diff --git a/src/Ookii.CommandLine/TypeHelper.cs b/src/Ookii.CommandLine/TypeHelper.cs index 242fcbb1..145a1d66 100644 --- a/src/Ookii.CommandLine/TypeHelper.cs +++ b/src/Ookii.CommandLine/TypeHelper.cs @@ -12,7 +12,11 @@ static class TypeHelper { private const string ParseMethodName = "Parse"; - public static Type? FindGenericInterface(this Type type, Type interfaceType) + public static Type? FindGenericInterface( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + this Type type, Type interfaceType) { if (type == null) { @@ -37,7 +41,11 @@ static class TypeHelper return type.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType); } - public static bool ImplementsInterface(this Type type, Type interfaceType) + public static bool ImplementsInterface( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] +#endif + this Type type, Type interfaceType) { if (type == null) { @@ -52,7 +60,11 @@ public static bool ImplementsInterface(this Type type, Type interfaceType) return type.GetInterfaces().Any(i => i == interfaceType); } - public static object? CreateInstance(this Type type) + public static object? CreateInstance( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif + this Type type) { if (type == null) { @@ -62,7 +74,11 @@ public static bool ImplementsInterface(this Type type, Type interfaceType) return Activator.CreateInstance(type); } - public static object? CreateInstance(this Type type, params object?[]? args) + public static object? CreateInstance( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + this Type type, params object?[]? args) { if (type == null) { @@ -72,6 +88,9 @@ public static bool ImplementsInterface(this Type type, Type interfaceType) return Activator.CreateInstance(type, args); } +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] +#endif public static ArgumentConverter GetStringConverter(this Type type, Type? converterType) { if (type == null) @@ -87,7 +106,7 @@ public static ArgumentConverter GetStringConverter(this Type type, Type? convert if (converterType == null) { - var underlyingType = type.GetUnderlyingType(); + var underlyingType = type.IsNullableValueType() ? type.GetGenericArguments()[0] : type; converter = GetDefaultConverter(underlyingType); if (converter != null) { @@ -108,7 +127,13 @@ public static bool IsNullableValueType(this Type type) public static Type GetUnderlyingType(this Type type) => type.IsNullableValueType() ? type.GetGenericArguments()[0] : type; - private static ArgumentConverter? GetDefaultConverter(this Type 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)) { From 49c8ed84768c8519870b321330d49a796e7d3da2 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 24 Apr 2023 18:13:34 -0700 Subject: [PATCH 045/234] Fix formatting. --- src/Ookii.CommandLine/TypeHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine/TypeHelper.cs b/src/Ookii.CommandLine/TypeHelper.cs index 145a1d66..58b9747a 100644 --- a/src/Ookii.CommandLine/TypeHelper.cs +++ b/src/Ookii.CommandLine/TypeHelper.cs @@ -14,7 +14,7 @@ static class TypeHelper public static Type? FindGenericInterface( #if NET6_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] #endif this Type type, Type interfaceType) { @@ -43,7 +43,7 @@ static class TypeHelper public static bool ImplementsInterface( #if NET6_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] #endif this Type type, Type interfaceType) { From c16ed5daa840b9b2b5e2f4ff47e292efce4eebc3 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 24 Apr 2023 18:15:33 -0700 Subject: [PATCH 046/234] Fix message. --- src/Ookii.CommandLine/Commands/CommandManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index ff578ea6..c2c5f071 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -62,7 +62,7 @@ public class CommandManager /// the default options. /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] + [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandProviderAttribute instead.")] #endif public CommandManager(CommandOptions? options = null) : this(Assembly.GetCallingAssembly(), options) From 501c371fdb9f1ff923366c0f93eccf51ce72704a Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 26 Apr 2023 17:30:47 -0700 Subject: [PATCH 047/234] Support C# 11 required properties. --- src/Ookii.CommandLine.Generator/Extensions.cs | 2 +- .../ParserGenerator.cs | 69 +++++-- .../SourceBuilder.cs | 15 +- .../CommandLineParserNullableTest.cs | 193 +++--------------- .../NullableCommandTypes.cs | 189 +++++++++++++++++ src/Ookii.CommandLine/CommandLineArgument.cs | 39 +++- src/Ookii.CommandLine/CommandLineParser.cs | 23 ++- .../Commands/AutomaticVersionCommand.cs | 2 +- .../Support/ArgumentProvider.cs | 7 +- .../Support/GeneratedArgument.cs | 4 +- .../Support/ReflectionArgument.cs | 10 +- .../Support/ReflectionArgumentProvider.cs | 2 +- 12 files changed, 359 insertions(+), 196 deletions(-) create mode 100644 src/Ookii.CommandLine.Tests/NullableCommandTypes.cs diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 792f4068..a6d32f48 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -37,7 +37,7 @@ public static bool IsNullableValueType(this ITypeSymbol type) => type is INamedTypeSymbol namedType && namedType.IsNullableValueType(); public static bool AllowsNull(this ITypeSymbol type) - => (type is not INamedTypeSymbol namedType || namedType.IsNullableValueType()) || (type.IsReferenceType && type.NullableAnnotation != NullableAnnotation.NotAnnotated); + => (type is INamedTypeSymbol namedType && namedType.IsNullableValueType()) || (type.IsReferenceType && type.NullableAnnotation != NullableAnnotation.NotAnnotated); public static INamedTypeSymbol GetUnderlyingType(this INamedTypeSymbol type) => type.IsNullableValueType() ? (INamedTypeSymbol)type.TypeArguments[0] : type; diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 09944054..65e9318d 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -139,26 +139,17 @@ private void GenerateProvider() _builder.AppendLine("{}"); _builder.AppendLine(); _builder.AppendLine($"public override bool IsCommand => {isCommand.ToCSharpString()};"); - _builder.AppendLine(); - if (_argumentsClass.FindConstructor(_typeHelper.CommandLineParser) != null) - { - _builder.AppendLine($"public override object CreateInstance(Ookii.CommandLine.CommandLineParser parser) => new {_argumentsClass.Name}(parser);"); - } - else - { - _builder.AppendLine($"public override object CreateInstance(Ookii.CommandLine.CommandLineParser parser) => new {_argumentsClass.Name}();"); - } - _builder.AppendLine(); _builder.AppendLine("public override System.Collections.Generic.IEnumerable GetArguments(Ookii.CommandLine.CommandLineParser parser)"); _builder.OpenBlock(); var current = _argumentsClass; + List<(string, string, string)>? requiredProperties = null; while (current != null && current.SpecialType != SpecialType.System_Object) { foreach (var member in current.GetMembers()) { - GenerateArgument(member); + GenerateArgument(member, ref requiredProperties); } current = current.BaseType; @@ -167,10 +158,47 @@ private void GenerateProvider() // 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(); // GeneratedProvider class } - private void GenerateArgument(ISymbol member) + private void 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)) @@ -325,6 +353,12 @@ private void GenerateArgument(ISymbol member) elementTypeWithNullable = multiValueElementType!.WithNullableAnnotation(NullableAnnotation.NotAnnotated); namedElementTypeWithNullable = elementTypeWithNullable as INamedTypeSymbol; } + + if (property.IsRequired) + { + requiredProperties ??= new(); + requiredProperties.Add((member.Name, property.Type.ToDisplayString(), notNullAnnotation)); + } } else { @@ -398,14 +432,15 @@ private void GenerateArgument(ISymbol member) _builder.AppendLine($", validationAttributes: new Ookii.CommandLine.Validation.ArgumentValidationAttribute[] {{ {string.Join(", ", validators.Select(a => a.CreateInstantiation()))} }}"); } - if (property?.SetMethod?.DeclaredAccessibility == Accessibility.Public) - { - _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.ToDisplayString()})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"); - } - if (property != null) { + if (property.SetMethod?.DeclaredAccessibility == Accessibility.Public) + { + _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.ToDisplayString()})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"); + } + _builder.AppendLine($", getProperty: (target) => (({_argumentsClass.ToDisplayString()})target).{member.Name}"); + _builder.AppendLine($", requiredProperty: {property.IsRequired.ToCSharpString()}"); } if (methodInfo is MethodArgumentInfo info) diff --git a/src/Ookii.CommandLine.Generator/SourceBuilder.cs b/src/Ookii.CommandLine.Generator/SourceBuilder.cs index 69fa4366..deb9f09b 100644 --- a/src/Ookii.CommandLine.Generator/SourceBuilder.cs +++ b/src/Ookii.CommandLine.Generator/SourceBuilder.cs @@ -7,6 +7,7 @@ internal class SourceBuilder { private readonly StringBuilder _builder = new(); private int _indentLevel; + private bool _startOfLine = true; public SourceBuilder(INamespaceSymbol ns) : this(ns.IsGlobalNamespace ? null : ns.ToDisplayString()) @@ -25,15 +26,24 @@ public SourceBuilder(string? ns) } } + 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 OpenBlock() @@ -64,6 +74,9 @@ public string GetSource() private void WriteIndent() { - _builder.Append(' ', _indentLevel * 4); + if (_startOfLine) + { + _builder.Append(' ', _indentLevel * 4); + } } } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs index 5fc9eec7..2b24f5ed 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -1,6 +1,4 @@ -// 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 @@ -9,6 +7,7 @@ using Ookii.CommandLine.Support; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Reflection; @@ -140,9 +139,33 @@ public void TestNonNullableDictionary(ProviderKind kind) 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); + } +#if NET7_0_OR_GREATER + + [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); } +#endif + private static void ExpectNullException(CommandLineParser parser, string argumentName, params string[] args) { try @@ -157,9 +180,10 @@ private static void ExpectNullException(CommandLineParser parser, string argumen } } - private static NullableArguments ExpectSuccess(CommandLineParser parser, params string[] args) + private static T ExpectSuccess(CommandLineParser parser, params string[] args) + where T : class { - var result = (NullableArguments?)parser.Parse(args); + var result = parser.Parse(args); Assert.IsNotNull(result); return result; } @@ -175,165 +199,6 @@ public static IEnumerable ProviderKinds new object[] { ProviderKind.Generated } }; } - - 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 - { - // TODO: Put back with new ctor approach. - //public TestArguments( - // [ArgumentConverter(typeof(NullReturningStringConverter))] string? constructorNullable, - // [ArgumentConverter(typeof(NullReturningStringConverter))] string constructorNonNullable, - // [ArgumentConverter(typeof(NullReturningIntConverter))] int constructorValueType, - // [ArgumentConverter(typeof(NullReturningIntConverter))] int? constructorNullableValueType) - //{ - // ConstructorNullable = constructorNullable; - // ConstructorNonNullable = constructorNonNullable; - // ConstructorValueType = constructorValueType; - // ConstructorNullableValueType = constructorNullableValueType; - //} - - [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 diff --git a/src/Ookii.CommandLine.Tests/NullableCommandTypes.cs b/src/Ookii.CommandLine.Tests/NullableCommandTypes.cs new file mode 100644 index 00000000..f4c6d2b4 --- /dev/null +++ b/src/Ookii.CommandLine.Tests/NullableCommandTypes.cs @@ -0,0 +1,189 @@ +#if NET6_0_OR_GREATER +#nullable enable + +using Ookii.CommandLine.Conversion; +using System.Collections.Generic; +using System.Globalization; + +namespace Ookii.CommandLine.Tests; + +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 +{ + // TODO: Put back with new ctor approach. + //public TestArguments( + // [ArgumentConverter(typeof(NullReturningStringConverter))] string? constructorNullable, + // [ArgumentConverter(typeof(NullReturningStringConverter))] string constructorNonNullable, + // [ArgumentConverter(typeof(NullReturningIntConverter))] int constructorValueType, + // [ArgumentConverter(typeof(NullReturningIntConverter))] int? constructorNullableValueType) + //{ + // ConstructorNullable = constructorNullable; + // ConstructorNonNullable = constructorNonNullable; + // ConstructorValueType = constructorValueType; + // ConstructorNullableValueType = constructorNullableValueType; + //} + + [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; set; } + + [CommandLineArgument] + public required int[] Bar { get; set; } +} + +#endif diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 241da7b4..f0e9ee4f 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -276,6 +276,7 @@ internal struct ArgumentInfo 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 string? Description { get; set; } public string? ValueDescription { get; set; } @@ -372,6 +373,7 @@ internal CommandLineArgument(ArgumentInfo info) _valueType = info.ValueType; _description = info.Description; _isRequired = info.IsRequired; + IsRequiredProperty = info.IsRequiredProperty; _allowNull = info.AllowNull; _cancelParsing = info.CancelParsing; _validators = info.Validators; @@ -632,6 +634,21 @@ public bool IsRequired get { return _isRequired; } } + /// + /// 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; } + /// /// Gets the default value for an argument. /// @@ -1188,12 +1205,19 @@ protected virtual string DetermineValueDescription(Type? type = null) return Parser.Options.ValueDescriptionTransform?.Apply(typeName) ?? typeName; } - internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, Type argumentType, bool allowsNull, - string memberName, CommandLineArgumentAttribute attribute, - MultiValueSeparatorAttribute? multiValueSeparatorAttribute, DescriptionAttribute? descriptionAttribute, - bool allowDuplicateDictionaryKeys, KeyValueSeparatorAttribute? keyValueSeparatorAttribute, - IEnumerable? aliasAttributes, IEnumerable? shortAliasAttributes, - IEnumerable? validationAttributes) + internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, + Type argumentType, + bool allowsNull, + bool requiredProperty, + string memberName, + CommandLineArgumentAttribute attribute, + MultiValueSeparatorAttribute? multiValueSeparatorAttribute, + DescriptionAttribute? descriptionAttribute, + bool allowDuplicateDictionaryKeys, + KeyValueSeparatorAttribute? keyValueSeparatorAttribute, + IEnumerable? aliasAttributes, + IEnumerable? shortAliasAttributes, + IEnumerable? validationAttributes) { var argumentName = DetermineArgumentName(attribute.ArgumentName, memberName, parser.Options.ArgumentNameTransform); return new ArgumentInfo() @@ -1215,7 +1239,8 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, Type a Aliases = GetAliases(aliasAttributes, argumentName), ShortAliases = GetShortAliases(shortAliasAttributes, argumentName), DefaultValue = attribute.DefaultValue, - IsRequired = attribute.IsRequired, + IsRequired = attribute.IsRequired || requiredProperty, + IsRequiredProperty = requiredProperty, MemberName = memberName, AllowNull = allowsNull, CancelParsing = attribute.CancelParsing, diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 628e9f57..d6ee11f9 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -203,6 +203,7 @@ private struct PrefixInfo private ReadOnlyCollection? _argumentsReadOnlyWrapper; private ReadOnlyCollection? _argumentNamePrefixesReadOnlyWrapper; + private List? _requiredPropertyArguments; /// /// Gets the default character used to separate the name and the value of an argument. @@ -1346,6 +1347,16 @@ private void AddNamedArgument(CommandLineArgument argument) } } + // 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) + { + _requiredPropertyArguments ??= new(); + _requiredPropertyArguments.Add(argument); + } + _arguments.Add(argument); } @@ -1456,7 +1467,17 @@ private void VerifyPositionalArgumentRules() // TODO: Integrate with new ctor argument support. try { - commandLineArguments = _provider.CreateInstance(this); + object?[]? requiredPropertyValues = null; + if (_requiredPropertyArguments != null) + { + requiredPropertyValues = new object?[_requiredPropertyArguments.Count]; + for (int i = 0; i < requiredPropertyValues.Length; ++i) + { + requiredPropertyValues[i] = _requiredPropertyArguments[i].Value; + } + } + + commandLineArguments = _provider.CreateInstance(this, requiredPropertyValues); } catch (TargetInvocationException ex) { diff --git a/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs b/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs index 9fb906ba..60ad1426 100644 --- a/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs +++ b/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs @@ -24,7 +24,7 @@ public ArgumentProvider(LocalizedStringProvider stringProvider) public override string Description => _stringProvider.AutomaticVersionCommandDescription(); - public override object CreateInstance(CommandLineParser parser) => new AutomaticVersionCommand(parser); + public override object CreateInstance(CommandLineParser parser, object?[]? requiredPropertyValues) => new AutomaticVersionCommand(parser); public override IEnumerable GetArguments(CommandLineParser parser) { diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs index f393d73e..97cb4e8b 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -113,6 +113,11 @@ public void RunValidators(CommandLineParser 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 . - public abstract object CreateInstance(CommandLineParser parser); + public abstract object CreateInstance(CommandLineParser parser, object?[]? requiredPropertyValues); } diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 7f87d24c..d2ceea02 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -41,6 +41,7 @@ private GeneratedArgument(ArgumentInfo info, Action? setPropert /// /// /// + /// /// /// /// @@ -61,6 +62,7 @@ public static GeneratedArgument Create(CommandLineParser parser, ArgumentKind kind, ArgumentConverter converter, bool allowsNull, + bool requiredProperty = false, Type? keyType = null, Type? valueType = null, MultiValueSeparatorAttribute? multiValueSeparatorAttribute = null, @@ -74,7 +76,7 @@ public static GeneratedArgument Create(CommandLineParser parser, Func? getProperty = null, Func? callMethod = null) { - var info = CreateArgumentInfo(parser, argumentType, allowsNull, memberName, attribute, + var info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, memberName, attribute, multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index 3928e153..9542ffd4 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -9,6 +9,7 @@ using Ookii.CommandLine.Validation; using System.Threading; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace Ookii.CommandLine.Support; @@ -139,8 +140,15 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo 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, member.Name, attribute, multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); + ArgumentInfo info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, member.Name, attribute, + multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, + aliasAttributes, shortAliasAttributes, validationAttributes); DetermineAdditionalInfo(ref info, member); return new ReflectionArgument(info, property, method); diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs index 7ec68ab5..961d55df 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -38,7 +38,7 @@ public override string ApplicationFriendlyName public override bool IsCommand => CommandInfo.IsCommand(ArgumentsType); - public override object CreateInstance(CommandLineParser parser) + public override object CreateInstance(CommandLineParser parser, object?[]? requiredPropertyValues) { var inject = ArgumentsType.GetConstructor(new[] { typeof(CommandLineParser) }) != null; if (inject) From 1336a804b1db5c2a69434072c4b717ecf26ef9c4 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 26 Apr 2023 17:48:12 -0700 Subject: [PATCH 048/234] Support init accessors for required properties only. --- src/Ookii.CommandLine.Generator/Diagnostics.cs | 8 ++++++++ .../ParserGenerator.cs | 8 +++++++- .../Properties/Resources.Designer.cs | 18 ++++++++++++++++++ .../Properties/Resources.resx | 6 ++++++ ...ommandTypes.cs => NullableArgumentTypes.cs} | 2 +- src/Ookii.CommandLine/CommandLineArgument.cs | 6 ++++-- 6 files changed, 44 insertions(+), 4 deletions(-) rename src/Ookii.CommandLine.Tests/{NullableCommandTypes.cs => NullableArgumentTypes.cs} (96%) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 2e9d68d7..a6644cb5 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -79,6 +79,14 @@ public static Diagnostic ArgumentsClassIsNested(INamedTypeSymbol symbol) => Crea symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic NonRequiredInitOnlyProperty(IPropertySymbol property) => CreateDiagnostic( + "CL0009", + nameof(Resources.NonRequiredInitOnlyPropertyTitle), + nameof(Resources.NonRequiredInitOnlyPropertyMessageFormat), + DiagnosticSeverity.Error, + property.Locations.FirstOrDefault(), + property.ContainingType?.ToDisplayString(), property.Name); + public static Diagnostic UnknownAttribute(AttributeData attribute) => CreateDiagnostic( "CLW0001", nameof(Resources.UnknownAttributeTitle), diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 65e9318d..c96763ac 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -354,6 +354,12 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> namedElementTypeWithNullable = elementTypeWithNullable as INamedTypeSymbol; } + if (property.SetMethod != null && property.SetMethod.IsInitOnly && !property.IsRequired) + { + _context.ReportDiagnostic(Diagnostics.NonRequiredInitOnlyProperty(property)); + return; + } + if (property.IsRequired) { requiredProperties ??= new(); @@ -434,7 +440,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> if (property != null) { - if (property.SetMethod?.DeclaredAccessibility == Accessibility.Public) + if (property.SetMethod != null && property.SetMethod.DeclaredAccessibility == Accessibility.Public && !property.SetMethod.IsInitOnly) { _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.ToDisplayString()})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"); } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 8c943fa1..005b7b5c 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -240,6 +240,24 @@ internal static string NonPublicStaticMethodTitle { } } + /// + /// Looks up a localized string similar to The command line argument property {0}.{1} 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 property {0}.{1} must have a public set accessor.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index ad487c75..c0e595b5 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -177,6 +177,12 @@ Methods that are not public and static will be ignored. + + The command line argument property {0}.{1} may only have an 'init' accessor if the property is also declared as 'required'. + + + Init accessors may only be used on required properties. + The property {0}.{1} must have a public set accessor. diff --git a/src/Ookii.CommandLine.Tests/NullableCommandTypes.cs b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs similarity index 96% rename from src/Ookii.CommandLine.Tests/NullableCommandTypes.cs rename to src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs index f4c6d2b4..257f9b58 100644 --- a/src/Ookii.CommandLine.Tests/NullableCommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs @@ -180,7 +180,7 @@ partial class RequiredPropertyArguments // IsRequired is ignored [CommandLineArgument(IsRequired = false)] - public required string? Foo { get; set; } + public required string? Foo { get; init; } [CommandLineArgument] public required int[] Bar { get; set; } diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index f0e9ee4f..3304b6ac 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1,5 +1,6 @@ // Copyright (c) Sven Groot (Ookii.org) using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Support; using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; @@ -1449,9 +1450,10 @@ internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParse internal void ApplyPropertyValue(object target) { - // Do nothing for method-based values. + // Do nothing for method-based values, or for required properties if the provider is not + // using reflection. // TODO: Handle new style constructor parameters. - if (Kind == ArgumentKind.Method) + if (Kind == ArgumentKind.Method || (IsRequiredProperty && _parser.ProviderKind != ProviderKind.Reflection)) { return; } From aefa0e8fdc7d2c11abcde2f6dea79bce747582b5 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 28 Apr 2023 17:52:36 -0700 Subject: [PATCH 049/234] Register all commands in generated CommandProvider, not just generated ones. --- .../ArgumentsClassAttributes.cs | 41 +++++++++ .../CommandGenerator.cs | 33 ++++--- src/Ookii.CommandLine.Generator/Extensions.cs | 25 ++++++ .../ParserGenerator.cs | 87 +++++-------------- .../ParserIncrementalGenerator.cs | 40 +++++++-- src/Ookii.CommandLine.Tests/CommandTypes.cs | 5 +- 6 files changed, 146 insertions(+), 85 deletions(-) create mode 100644 src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs diff --git a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs new file mode 100644 index 00000000..f88ce786 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs @@ -0,0 +1,41 @@ +using Microsoft.CodeAnalysis; + +namespace Ookii.CommandLine.Generator; + +internal readonly struct ArgumentsClassAttributes +{ + private readonly AttributeData? _parseOptions; + private readonly AttributeData? _description; + private readonly AttributeData? _applicationFriendlyName; + private readonly AttributeData? _command; + private readonly AttributeData? _generatedParser; + private readonly List? _classValidators; + private readonly List? _aliases; + + public ArgumentsClassAttributes(ISymbol symbol, TypeHelper typeHelper, SourceProductionContext context) + { + foreach (var attribute in symbol.GetAttributes()) + { + if (attribute.CheckType(typeHelper.ParseOptionsAttribute, ref _parseOptions) || + attribute.CheckType(typeHelper.DescriptionAttribute, ref _description) || + attribute.CheckType(typeHelper.ApplicationFriendlyNameAttribute, ref _applicationFriendlyName) || + attribute.CheckType(typeHelper.CommandAttribute, ref _command) || + attribute.CheckType(typeHelper.ClassValidationAttribute, ref _classValidators) || + attribute.CheckType(typeHelper.AliasAttribute, ref _aliases) || + attribute.CheckType(typeHelper.GeneratedParserAttribute, ref _generatedParser)) + { + continue; + } + + context.ReportDiagnostic(Diagnostics.UnknownAttribute(attribute)); + } + } + + public AttributeData? ParseOptions => _parseOptions; + public AttributeData? Description => _description; + public AttributeData? ApplicationFriendlyName => _applicationFriendlyName; + public AttributeData? Command => _command; + public AttributeData? GeneratedParser => _generatedParser; + public List? ClassValidators => _classValidators; + public List? Aliases => _aliases; +} diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index 902784a2..afc55048 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -8,8 +8,7 @@ internal class CommandGenerator { private readonly TypeHelper _typeHelper; private readonly SourceProductionContext _context; - private readonly List<(INamedTypeSymbol Type, AttributeData CommandAttribute, AttributeData? DescriptionAttribute, - List? AliasAttributes)> _commands = new(); + private readonly List<(INamedTypeSymbol Type, ArgumentsClassAttributes? Attributes)> _commands = new(); private readonly List _providers = new(); @@ -19,9 +18,14 @@ public CommandGenerator(TypeHelper typeHelper, SourceProductionContext context) _context = context; } - public void AddCommand(INamedTypeSymbol type, AttributeData commandAttribute, AttributeData? descriptionAttribute, List? aliasAttributes) + public void AddGeneratedCommand(INamedTypeSymbol type, ArgumentsClassAttributes attributes) { - _commands.Add((type, commandAttribute, descriptionAttribute, aliasAttributes)); + _commands.Add((type, attributes)); + } + + public void AddCommand(INamedTypeSymbol type) + { + _commands.Add((type, null)); } public void AddProvider(INamedTypeSymbol provider) @@ -75,6 +79,7 @@ public void Generate() // TODO: Providers with custom command lists. foreach (var command in _commands) { + var isGenerated = command.Attributes != null; var useCustomParsing = command.Type.ImplementsInterface(_typeHelper.ICommandWithCustomParsing); var commandTypeName = command.Type.ToDisplayString(); if (useCustomParsing) @@ -93,20 +98,28 @@ public void Generate() builder.AppendLine($", typeof({commandTypeName})"); } - builder.AppendLine($", {command.CommandAttribute.CreateInstantiation()}"); - if (command.DescriptionAttribute != null) + var attributes = command.Attributes ?? new ArgumentsClassAttributes(command.Type, _typeHelper, _context); + builder.AppendLine($", {attributes.Command!.CreateInstantiation()}"); + if (attributes.Description != null) { - builder.AppendLine($", descriptionAttribute: {command.DescriptionAttribute.CreateInstantiation()}"); + builder.AppendLine($", descriptionAttribute: {attributes.Description.CreateInstantiation()}"); } - if (command.AliasAttributes != null) + if (attributes.Aliases != null) { - builder.AppendLine($", aliasAttributes: new Ookii.CommandLine.AliasAttribute[] {{ {string.Join(", ", command.AliasAttributes.Select(a => a.CreateInstantiation()))} }}"); + builder.AppendLine($", aliasAttributes: new Ookii.CommandLine.AliasAttribute[] {{ {string.Join(", ", attributes.Aliases.Select(a => a.CreateInstantiation()))} }}"); } if (!useCustomParsing) { - builder.AppendLine($", createParser: options => {commandTypeName}.CreateParser(options)"); + if (isGenerated) + { + builder.AppendLine($", createParser: options => {commandTypeName}.CreateParser(options)"); + } + else + { + builder.AppendLine($", createParser: options => new CommandLineParser<{commandTypeName}>(options)"); + } } builder.DecreaseIndent(); diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index a6d32f48..0d2a3eb7 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -154,4 +154,29 @@ public static string ToIdentifier(this string displayName, string suffix) return null; } + + // Using a ref parameter with bool return allows me to chain these together. + public static bool CheckType(this AttributeData data, ITypeSymbol? attributeType, ref AttributeData? attribute) + { + if (attribute != null || !(data.AttributeClass?.DerivesFrom(attributeType) ?? false)) + { + return false; + } + + attribute = data; + return true; + } + + // Using a ref parameter with bool return allows me to chain these together. + public static bool CheckType(this AttributeData data, ITypeSymbol? attributeType, ref List? attributes) + { + if (!(data.AttributeClass?.DerivesFrom(attributeType) ?? false)) + { + return false; + } + + attributes ??= new(); + attributes.Add(data); + return true; + } } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index c96763ac..e3021bf1 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -72,42 +72,20 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen private void GenerateProvider() { - // Find the attribute that can apply to an arguments class. + // 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. - AttributeData? parseOptions = null; - AttributeData? description = null; - AttributeData? applicationFriendlyName = null; - AttributeData? commandAttribute = null; - List? classValidators = null; - List? aliasAttributes = null; // for command usage. - foreach (var attribute in _argumentsClass.GetAttributes()) - { - if (CheckAttribute(attribute, _typeHelper.ParseOptionsAttribute, ref parseOptions) || - CheckAttribute(attribute, _typeHelper.DescriptionAttribute, ref description) || - CheckAttribute(attribute, _typeHelper.ApplicationFriendlyNameAttribute, ref applicationFriendlyName) || - CheckAttribute(attribute, _typeHelper.CommandAttribute, ref commandAttribute) || - CheckAttribute(attribute, _typeHelper.ClassValidationAttribute, ref classValidators) || - CheckAttribute(attribute, _typeHelper.AliasAttribute, ref aliasAttributes)) - { - continue; - } - - if (!attribute.AttributeClass?.DerivesFrom(_typeHelper.GeneratedParserAttribute) ?? false) - { - _context.ReportDiagnostic(Diagnostics.UnknownAttribute(attribute)); - } - } + var attributes = new ArgumentsClassAttributes(_argumentsClass, _typeHelper, _context); // TODO: Warn if AliasAttribute without CommandAttribute. var isCommand = false; - if (commandAttribute != null) + if (attributes.Command != null) { if (_argumentsClass.ImplementsInterface(_typeHelper.ICommand)) { isCommand = true; - _commandGenerator.AddCommand(_argumentsClass, commandAttribute, description, aliasAttributes); + _commandGenerator.AddGeneratedCommand(_argumentsClass, attributes); } else { @@ -123,18 +101,18 @@ private void GenerateProvider() _builder.AppendLine("public GeneratedProvider()"); _builder.IncreaseIndent(); _builder.AppendLine($": base(typeof({_argumentsClass.Name}),"); - _builder.AppendLine($" {parseOptions?.CreateInstantiation() ?? "null"},"); - if (classValidators == null) + _builder.AppendLine($" {attributes.ParseOptions?.CreateInstantiation() ?? "null"},"); + if (attributes.ClassValidators == null) { _builder.AppendLine($" null,"); } else { - _builder.AppendLine($" new Ookii.CommandLine.Validation.ClassValidationAttribute[] {{ {string.Join(", ", classValidators.Select(v => v.CreateInstantiation()))} }},"); + _builder.AppendLine($" new Ookii.CommandLine.Validation.ClassValidationAttribute[] {{ {string.Join(", ", attributes.ClassValidators.Select(v => v.CreateInstantiation()))} }},"); } - _builder.AppendLine($" {applicationFriendlyName?.CreateInstantiation() ?? "null"},"); - _builder.AppendLine($" {description?.CreateInstantiation() ?? "null"})"); + _builder.AppendLine($" {attributes.ApplicationFriendlyName?.CreateInstantiation() ?? "null"},"); + _builder.AppendLine($" {attributes.Description?.CreateInstantiation() ?? "null"})"); _builder.DecreaseIndent(); _builder.AppendLine("{}"); _builder.AppendLine(); @@ -219,17 +197,17 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> List? validators = null; foreach (var attribute in member.GetAttributes()) { - if (CheckAttribute(attribute, _typeHelper.CommandLineArgumentAttribute, ref commandLineArgumentAttribute) || - CheckAttribute(attribute, _typeHelper.MultiValueSeparatorAttribute, ref multiValueSeparator) || - CheckAttribute(attribute, _typeHelper.DescriptionAttribute, ref description) || - CheckAttribute(attribute, _typeHelper.AllowDuplicateDictionaryKeysAttribute, ref allowDuplicateDictionaryKeys) || - CheckAttribute(attribute, _typeHelper.KeyValueSeparatorAttribute, ref keyValueSeparator) || - CheckAttribute(attribute, _typeHelper.ArgumentConverterAttribute, ref converterAttribute) || - CheckAttribute(attribute, _typeHelper.KeyConverterAttribute, ref keyConverterAttribute) || - CheckAttribute(attribute, _typeHelper.ValueConverterAttribute, ref valueConverterAttribute) || - CheckAttribute(attribute, _typeHelper.AliasAttribute, ref aliases) || - CheckAttribute(attribute, _typeHelper.ShortAliasAttribute, ref shortAliases) || - CheckAttribute(attribute, _typeHelper.ArgumentValidationAttribute, ref validators)) + if (attribute.CheckType(_typeHelper.CommandLineArgumentAttribute, ref commandLineArgumentAttribute) || + attribute.CheckType(_typeHelper.MultiValueSeparatorAttribute, ref multiValueSeparator) || + attribute.CheckType(_typeHelper.DescriptionAttribute, ref description) || + attribute.CheckType(_typeHelper.AllowDuplicateDictionaryKeysAttribute, ref allowDuplicateDictionaryKeys) || + attribute.CheckType(_typeHelper.KeyValueSeparatorAttribute, ref keyValueSeparator) || + attribute.CheckType(_typeHelper.ArgumentConverterAttribute, ref converterAttribute) || + attribute.CheckType(_typeHelper.KeyConverterAttribute, ref keyConverterAttribute) || + attribute.CheckType(_typeHelper.ValueConverterAttribute, ref valueConverterAttribute) || + attribute.CheckType(_typeHelper.AliasAttribute, ref aliases) || + attribute.CheckType(_typeHelper.ShortAliasAttribute, ref shortAliases) || + attribute.CheckType(_typeHelper.ArgumentValidationAttribute, ref validators)) { continue; } @@ -482,31 +460,6 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.AppendLine(");"); } - // Using a ref parameter with bool return allows me to chain these together. - private static bool CheckAttribute(AttributeData data, ITypeSymbol? attributeType, ref AttributeData? attribute) - { - if (attribute != null || !(data.AttributeClass?.DerivesFrom(attributeType) ?? false)) - { - return false; - } - - attribute = data; - return true; - } - - // Using a ref parameter with bool return allows me to chain these together. - private static bool CheckAttribute(AttributeData data, ITypeSymbol? attributeType, ref List? attributes) - { - if (!(data.AttributeClass?.DerivesFrom(attributeType) ?? false)) - { - return false; - } - - attributes ??= new(); - attributes.Add(data); - return true; - } - private (ITypeSymbol?, INamedTypeSymbol?, ITypeSymbol?)? DetermineMultiValueType(IPropertySymbol property, ITypeSymbol argumentType) { if (argumentType is INamedTypeSymbol namedType) diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index b1a3c259..3ed01465 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -11,7 +11,14 @@ namespace Ookii.CommandLine.Generator; [Generator] public class ParserIncrementalGenerator : IIncrementalGenerator { - private record struct ClassInfo(ClassDeclarationSyntax Syntax, bool IsCommandProvider); + private enum ClassKind + { + Arguments, + CommandProvider, + Command, + } + + private record struct ClassInfo(ClassDeclarationSyntax Syntax, ClassKind ClassKind); public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -48,6 +55,22 @@ private static void Execute(Compilation compilation, ImmutableArray 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 + { + context.ReportDiagnostic(Diagnostics.CommandAttributeWithoutInterface(symbol)); + } + + continue; + } + // TODO: Custom messages for provider types. if (!symbol.IsReferenceType) { @@ -73,7 +96,7 @@ private static void Execute(Compilation compilation, ImmutableArray continue; } - if (info.IsCommandProvider) + if (info.ClassKind == ClassKind.CommandProvider) { commandGenerator.AddProvider(symbol); continue; @@ -101,6 +124,8 @@ private static void Execute(Compilation compilation, ImmutableArray var typeHelper = new TypeHelper(context.SemanticModel.Compilation); var generatedParserType = typeHelper.GeneratedParserAttribute; var generatedCommandProviderType = typeHelper.GeneratedCommandProviderAttribute; + var commandType = typeHelper.CommandAttribute; + var isCommand = false; foreach (var attributeList in classDeclaration.AttributeLists) { foreach (var attribute in attributeList.Attributes) @@ -114,16 +139,21 @@ private static void Execute(Compilation compilation, ImmutableArray var attributeType = attributeSymbol.ContainingType; if (attributeType.SymbolEquals(generatedParserType)) { - return new(classDeclaration, false); + return new(classDeclaration, ClassKind.Arguments); } if (attributeType.SymbolEquals(generatedCommandProviderType)) { - return new(classDeclaration, true); + return new(classDeclaration, ClassKind.CommandProvider); + } + + if (attributeType.SymbolEquals(commandType)) + { + isCommand = true; } } } - return null; + return isCommand ? new(classDeclaration, ClassKind.Command) : null; } } diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index a512e704..3ac3953e 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -37,7 +37,6 @@ public int Run() } } -[GeneratedParser] [Command("custom")] [Description("Custom parsing command.")] partial class CustomParsingCommand : ICommandWithCustomParsing @@ -66,7 +65,7 @@ public int Run() } // Hidden so I don't have to update the expected usage. -[GeneratedParser] +// Not generated to test registration of plain commands without generation. [Command(IsHidden = true)] [Description("Async command description.")] partial class AsyncCommand : IAsyncCommand @@ -77,7 +76,7 @@ partial class AsyncCommand : IAsyncCommand public int Run() { - // Do somehting different than RunAsync so the test can differentiate which one was + // Do something different than RunAsync so the test can differentiate which one was // called. return Value + 1; } From f521b63579bc6b16d9a19f0cb804deb153ecc5f2 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 28 Apr 2023 18:05:33 -0700 Subject: [PATCH 050/234] Raise error if GeneratedParserAttribute is used with ICommandWithCustomParsing. --- .../ArgumentsClassAttributes.cs | 2 +- .../Diagnostics.cs | 10 +++++++++- .../ParserGenerator.cs | 18 +++++++++++++---- .../Properties/Resources.Designer.cs | 20 ++++++++++++++++++- .../Properties/Resources.resx | 8 +++++++- 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs index f88ce786..fb919b10 100644 --- a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs +++ b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs @@ -27,7 +27,7 @@ public ArgumentsClassAttributes(ISymbol symbol, TypeHelper typeHelper, SourcePro continue; } - context.ReportDiagnostic(Diagnostics.UnknownAttribute(attribute)); + context.ReportDiagnostic(Diagnostics.IgnoredAttribute(attribute)); } } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index a6644cb5..3b0bdc01 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -87,7 +87,15 @@ public static Diagnostic NonRequiredInitOnlyProperty(IPropertySymbol property) = property.Locations.FirstOrDefault(), property.ContainingType?.ToDisplayString(), property.Name); - public static Diagnostic UnknownAttribute(AttributeData attribute) => CreateDiagnostic( + public static Diagnostic GeneratedCustomParsingCommand(INamedTypeSymbol symbol) => CreateDiagnostic( + "CL0010", + nameof(Resources.GeneratedCustomParsingCommandTitle), + nameof(Resources.GeneratedCustomParsingCommandMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + + public static Diagnostic IgnoredAttribute(AttributeData attribute) => CreateDiagnostic( "CLW0001", nameof(Resources.UnknownAttributeTitle), nameof(Resources.UnknownAttributeMessageFormat), diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index e3021bf1..5ddfb054 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -54,7 +54,11 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen } _builder.OpenBlock(); - GenerateProvider(); + if (!GenerateProvider()) + { + return null; + } + _builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{_argumentsClass.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new GeneratedProvider(), options);"); _builder.AppendLine(); var nullableType = _argumentsClass.WithNullableAnnotation(NullableAnnotation.Annotated); @@ -70,7 +74,7 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen return _builder.GetSource(); } - private void GenerateProvider() + private bool GenerateProvider() { // Find the attributes that can apply to an arguments class. // This code also finds attributes that inherit from those attribute. By instantiating the @@ -82,7 +86,12 @@ private void GenerateProvider() var isCommand = false; if (attributes.Command != null) { - if (_argumentsClass.ImplementsInterface(_typeHelper.ICommand)) + if (_argumentsClass.ImplementsInterface(_typeHelper.ICommandWithCustomParsing)) + { + _context.ReportDiagnostic(Diagnostics.GeneratedCustomParsingCommand(_argumentsClass)); + return false; + } + else if (_argumentsClass.ImplementsInterface(_typeHelper.ICommand)) { isCommand = true; _commandGenerator.AddGeneratedCommand(_argumentsClass, attributes); @@ -174,6 +183,7 @@ private void GenerateProvider() _builder.CloseBlock(); // CreateInstance() _builder.CloseBlock(); // GeneratedProvider class + return true; } private void GenerateArgument(ISymbol member, ref List<(string, string, string)>? requiredProperties) @@ -212,7 +222,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> continue; } - _context.ReportDiagnostic(Diagnostics.UnknownAttribute(attribute)); + _context.ReportDiagnostic(Diagnostics.IgnoredAttribute(attribute)); } // Check if it is an attribute. diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 005b7b5c..0b165e17 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -150,6 +150,24 @@ internal static string CommandAttributeWithoutInterfaceTitle { } } + /// + /// 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 multi-value command line argument defined by {0}.{1} must have an array rank of one.. /// @@ -196,7 +214,7 @@ internal static string NoConverterMessageFormat { } /// - /// Looks up a localized string similar to No acommand line rgument converter exists for the argument's type.. + /// Looks up a localized string similar to No command line argument converter exists for the argument's type.. /// internal static string NoConverterTitle { get { diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index c0e595b5..ed1e838b 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -147,6 +147,12 @@ The command line arguments class has the CommandAttribute but does not implement ICommand. + + 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 multi-value command line argument defined by {0}.{1} must have an array rank of one. @@ -163,7 +169,7 @@ No command line argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter. - No acommand line rgument converter exists for the argument's type. + No command line argument converter exists for the argument's type. The property {0}.{1} will not create a command line argument because it is not a public instance property. From 1e6eb43a4b84961a360b7cfae04bce9d6ce8337a Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 28 Apr 2023 18:12:58 -0700 Subject: [PATCH 051/234] Fixed: consider base class attributes for source generation. --- .../ArgumentsClassAttributes.cs | 28 +++++++++++-------- src/Ookii.CommandLine.Generator/Extensions.cs | 4 +-- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 1 + .../CommandLineParserTest.cs | 1 + 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs index fb919b10..382c946b 100644 --- a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs +++ b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs @@ -12,22 +12,26 @@ internal readonly struct ArgumentsClassAttributes private readonly List? _classValidators; private readonly List? _aliases; - public ArgumentsClassAttributes(ISymbol symbol, TypeHelper typeHelper, SourceProductionContext context) + public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper, SourceProductionContext context) { - foreach (var attribute in symbol.GetAttributes()) + // Exclude special types so we don't generate warnings for attributes on framework types. + for (var current = symbol; current?.SpecialType == SpecialType.None; current = current.BaseType) { - if (attribute.CheckType(typeHelper.ParseOptionsAttribute, ref _parseOptions) || - attribute.CheckType(typeHelper.DescriptionAttribute, ref _description) || - attribute.CheckType(typeHelper.ApplicationFriendlyNameAttribute, ref _applicationFriendlyName) || - attribute.CheckType(typeHelper.CommandAttribute, ref _command) || - attribute.CheckType(typeHelper.ClassValidationAttribute, ref _classValidators) || - attribute.CheckType(typeHelper.AliasAttribute, ref _aliases) || - attribute.CheckType(typeHelper.GeneratedParserAttribute, ref _generatedParser)) + foreach (var attribute in current.GetAttributes()) { - continue; - } + if (attribute.CheckType(typeHelper.ParseOptionsAttribute, ref _parseOptions) || + attribute.CheckType(typeHelper.DescriptionAttribute, ref _description) || + attribute.CheckType(typeHelper.ApplicationFriendlyNameAttribute, ref _applicationFriendlyName) || + attribute.CheckType(typeHelper.CommandAttribute, ref _command) || + attribute.CheckType(typeHelper.ClassValidationAttribute, ref _classValidators) || + attribute.CheckType(typeHelper.AliasAttribute, ref _aliases) || + attribute.CheckType(typeHelper.GeneratedParserAttribute, ref _generatedParser)) + { + continue; + } - context.ReportDiagnostic(Diagnostics.IgnoredAttribute(attribute)); + context.ReportDiagnostic(Diagnostics.IgnoredAttribute(attribute)); + } } } diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 0d2a3eb7..92447aaf 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -158,12 +158,12 @@ public static string ToIdentifier(this string displayName, string suffix) // Using a ref parameter with bool return allows me to chain these together. public static bool CheckType(this AttributeData data, ITypeSymbol? attributeType, ref AttributeData? attribute) { - if (attribute != null || !(data.AttributeClass?.DerivesFrom(attributeType) ?? false)) + if (!(data.AttributeClass?.DerivesFrom(attributeType) ?? false)) { return false; } - attribute = data; + attribute ??= data; return true; } diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index b661b5b0..14a751d2 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -578,6 +578,7 @@ partial class ConversionArguments public int? Nullable { get; set; } } +[Description("Base class attribute.")] class BaseArguments { [CommandLineArgument] diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index aa021fa1..edbfd691 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1125,6 +1125,7 @@ public void TestConversion(ProviderKind kind) public void TestDerivedClass(ProviderKind kind) { var parser = CreateParser(kind); + Assert.AreEqual("Base class attribute.", parser.Description); Assert.AreEqual(4, parser.Arguments.Count); VerifyArguments(parser.Arguments, new[] { From d0e6fda38527346ee09fa2e393ca88a42596aa06 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 1 May 2023 16:24:47 -0700 Subject: [PATCH 052/234] Move ValueDescription to its own attribute. --- .../ParserGenerator.cs | 7 + src/Ookii.CommandLine.Generator/TypeHelper.cs | 18 +- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 6 +- src/Ookii.CommandLine/CommandLineArgument.cs | 6 +- .../CommandLineArgumentAttribute.cs | 590 ++++++++---------- src/Ookii.CommandLine/ParseOptions.cs | 9 +- .../Support/GeneratedArgument.cs | 6 +- .../Support/ReflectionArgument.cs | 5 +- .../ValueDescriptionAttribute.cs | 80 +++ src/Samples/Parser/ProgramArguments.cs | 3 +- src/Samples/TrimTest/Program.cs | 3 +- src/Samples/Wpf/Arguments.cs | 3 +- 12 files changed, 397 insertions(+), 339 deletions(-) create mode 100644 src/Ookii.CommandLine/ValueDescriptionAttribute.cs diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 5ddfb054..2bd979b1 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -197,6 +197,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> AttributeData? commandLineArgumentAttribute = null; AttributeData? multiValueSeparator = null; AttributeData? description = null; + AttributeData? valueDescription = null; AttributeData? allowDuplicateDictionaryKeys = null; AttributeData? keyValueSeparator = null; AttributeData? converterAttribute = null; @@ -210,6 +211,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> if (attribute.CheckType(_typeHelper.CommandLineArgumentAttribute, ref commandLineArgumentAttribute) || attribute.CheckType(_typeHelper.MultiValueSeparatorAttribute, ref multiValueSeparator) || attribute.CheckType(_typeHelper.DescriptionAttribute, ref description) || + attribute.CheckType(_typeHelper.ValueDescriptionAttribute, ref valueDescription) || attribute.CheckType(_typeHelper.AllowDuplicateDictionaryKeysAttribute, ref allowDuplicateDictionaryKeys) || attribute.CheckType(_typeHelper.KeyValueSeparatorAttribute, ref keyValueSeparator) || attribute.CheckType(_typeHelper.ArgumentConverterAttribute, ref converterAttribute) || @@ -401,6 +403,11 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.AppendLine($", descriptionAttribute: {description.CreateInstantiation()}"); } + if (valueDescription != null) + { + _builder.AppendLine($", valueDescriptionAttribute: {valueDescription.CreateInstantiation()}"); + } + if (allowDuplicateDictionaryKeys != null) { _builder.AppendLine(", allowDuplicateDictionaryKeys: true"); diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index d7c56d63..14c3e8c0 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -47,36 +47,38 @@ public TypeHelper(Compilation compilation) public INamedTypeSymbol? GeneratedParserAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "GeneratedParserAttribute"); - public INamedTypeSymbol? GeneratedCommandProviderAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.GeneratedCommandProviderAttribute"); - public INamedTypeSymbol? CommandLineArgumentAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "CommandLineArgumentAttribute"); public INamedTypeSymbol? ParseOptionsAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "ParseOptionsAttribute"); public INamedTypeSymbol? ApplicationFriendlyNameAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "ApplicationFriendlyNameAttribute"); - public INamedTypeSymbol? CommandAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.CommandAttribute"); - - public INamedTypeSymbol? ClassValidationAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Validation.ClassValidationAttribute"); - public INamedTypeSymbol? MultiValueSeparatorAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "MultiValueSeparatorAttribute"); - public INamedTypeSymbol? KeyValueSeparatorAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Conversion.KeyValueSeparatorAttribute"); - 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? 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? CommandAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.CommandAttribute"); + + public INamedTypeSymbol? GeneratedCommandProviderAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.GeneratedCommandProviderAttribute"); + public INamedTypeSymbol? ICommand => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommand"); public INamedTypeSymbol? ICommandWithCustomParsing => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommandWithCustomParsing"); diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 14a751d2..d8b49c2d 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -31,7 +31,8 @@ partial class TestArguments [Description("Arg1 description.")] public string Arg1 { get; set; } - [CommandLineArgument("other", Position = 2, DefaultValue = 42, ValueDescription = "Number")] + [CommandLineArgument("other", Position = 2, DefaultValue = 42)] + [ValueDescription("Number")] [Description("Arg2 description.")] public int Arg2 { get; set; } @@ -42,7 +43,8 @@ partial class TestArguments public string Arg3 { get; set; } // Default value is intentionally a string to test default value conversion. - [CommandLineArgument("other2", DefaultValue = "47", ValueDescription = "Number", Position = 5), Description("Arg4 description.")] + [CommandLineArgument("other2", DefaultValue = "47", Position = 5), Description("Arg4 description.")] + [ValueDescription("Number")] [ValidateRange(0, 1000, IncludeInUsageHelp = false)] public int Arg4 { get; set; } diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 3304b6ac..54790770 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -715,7 +715,8 @@ public string Description /// using the property. /// /// - /// + /// + /// public string ValueDescription => _valueDescription ??= DetermineValueDescription(); /// @@ -1214,6 +1215,7 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, CommandLineArgumentAttribute attribute, MultiValueSeparatorAttribute? multiValueSeparatorAttribute, DescriptionAttribute? descriptionAttribute, + ValueDescriptionAttribute? valueDescriptionAttribute, bool allowDuplicateDictionaryKeys, KeyValueSeparatorAttribute? keyValueSeparatorAttribute, IEnumerable? aliasAttributes, @@ -1231,7 +1233,7 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, ArgumentType = argumentType, ElementTypeWithNullable = argumentType, Description = descriptionAttribute?.Description, - ValueDescription = attribute.ValueDescription, + ValueDescription = valueDescriptionAttribute?.ValueDescription, Position = attribute.Position < 0 ? null : attribute.Position, AllowDuplicateDictionaryKeys = allowDuplicateDictionaryKeys, MultiValueSeparator = GetMultiValueSeparator(multiValueSeparatorAttribute), diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index 4341e7ba..d0cf29cc 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -1,343 +1,303 @@ -// 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 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 . +/// +/// +/// 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 . +/// +/// +/// 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 . +/// +/// +/// +/// +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] +public sealed class CommandLineArgumentAttribute : Attribute { + private readonly string? _argumentName; + private bool _short; + /// - /// Indicates a property or method of a class defines a command line argument. + /// 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. + /// /// /// - /// 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. + /// 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 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 , + /// this is the long name of the argument. /// /// - /// If this attribute is applied to a method, that method must have one of the following - /// signatures: + /// If the property is + /// and the property is , the + /// property is ignored. /// - /// - /// 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(); - /// + /// + /// + 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 . + /// + /// /// - /// 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 . + /// This property is ignored if 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 + /// 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 is not + /// . + /// /// - /// 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 not set but this property is set to , + /// the short name will be derived using the first character of the long name. /// /// - /// - /// - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] - public 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; - - /// - /// 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 - /// . - /// - /// - /// 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; } + get => _short || ShortName != '\0'; + set => _short = value; + } - /// - /// 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 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 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 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 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 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 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 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 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. + /// + /// + /// 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 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/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index bdfa0795..e9d4d15c 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -451,11 +451,10 @@ public LocalizedStringProvider StringProvider /// describe the purpose of the argument. /// /// - /// If an argument doesn't have the - /// property set, 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. + /// 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. /// /// /// diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index d2ceea02..d29fc057 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -44,6 +44,7 @@ private GeneratedArgument(ArgumentInfo info, Action? setPropert /// /// /// + /// /// /// /// @@ -67,6 +68,7 @@ public static GeneratedArgument Create(CommandLineParser parser, Type? valueType = null, MultiValueSeparatorAttribute? multiValueSeparatorAttribute = null, DescriptionAttribute? descriptionAttribute = null, + ValueDescriptionAttribute? valueDescriptionAttribute = null, bool allowDuplicateDictionaryKeys = false, KeyValueSeparatorAttribute? keyValueSeparatorAttribute = null, IEnumerable? aliasAttributes = null, @@ -77,8 +79,8 @@ public static GeneratedArgument Create(CommandLineParser parser, Func? callMethod = null) { var info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, memberName, attribute, - multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, - aliasAttributes, shortAliasAttributes, validationAttributes); + multiValueSeparatorAttribute, descriptionAttribute, valueDescriptionAttribute, allowDuplicateDictionaryKeys, + keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); info.ElementType = elementType; info.ElementTypeWithNullable = elementTypeWithNullable; diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index 9542ffd4..ab3ad9a7 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -135,6 +135,7 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo 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(); @@ -147,8 +148,8 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo #endif ArgumentInfo info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, member.Name, attribute, - multiValueSeparatorAttribute, descriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, - aliasAttributes, shortAliasAttributes, validationAttributes); + multiValueSeparatorAttribute, descriptionAttribute, valueDescriptionAttribute, allowDuplicateDictionaryKeys, + keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); DetermineAdditionalInfo(ref info, member); return new ReflectionArgument(info, property, method); diff --git a/src/Ookii.CommandLine/ValueDescriptionAttribute.cs b/src/Ookii.CommandLine/ValueDescriptionAttribute.cs new file mode 100644 index 00000000..a1a17dac --- /dev/null +++ b/src/Ookii.CommandLine/ValueDescriptionAttribute.cs @@ -0,0 +1,80 @@ +using System; +using System.ComponentModel; + +namespace Ookii.CommandLine; + +/// +/// Supplies a short description of the arguments'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 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 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>". +/// +/// +/// 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 +{ + /// + /// Initializes a new instance of the attribute. + /// + /// + /// + 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 default implementation of the property returns the + /// value of this property. + /// + /// + protected string ValueDescriptionValue { get; } +} diff --git a/src/Samples/Parser/ProgramArguments.cs b/src/Samples/Parser/ProgramArguments.cs index 4b0519ff..aed9d7a8 100644 --- a/src/Samples/Parser/ProgramArguments.cs +++ b/src/Samples/Parser/ProgramArguments.cs @@ -73,7 +73,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; } diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 49180be5..00583b34 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -43,7 +43,8 @@ partial class Arguments : ICommand [ValidateNotEmpty] public string? Test { get; set; } - [CommandLineArgument(ValueDescription = "Stuff")] + [CommandLineArgument] + [ValueDescription("Stuff")] [KeyValueSeparator("==")] [MultiValueSeparator] public Dictionary Test2 { get; set; } = default!; diff --git a/src/Samples/Wpf/Arguments.cs b/src/Samples/Wpf/Arguments.cs index 8054b033..0b796e79 100644 --- a/src/Samples/Wpf/Arguments.cs +++ b/src/Samples/Wpf/Arguments.cs @@ -63,7 +63,8 @@ public static bool Version(CommandLineParser parser) [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; } From fb83bb44b01c5d3db6adec1d3adbf2dadcdf0c68 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 1 May 2023 16:38:18 -0700 Subject: [PATCH 053/234] Fix docs for DefaultValue. --- src/Ookii.CommandLine/CommandLineArgumentAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index d0cf29cc..c916cae1 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -231,8 +231,8 @@ public bool IsShort /// /// /// 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 . + /// includes the default value. To change that, set the + /// property to . /// /// /// From 9b8196fcc966f10556f8c75365156abbe9a0b120 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 1 May 2023 17:50:48 -0700 Subject: [PATCH 054/234] Diagnostic for ignored default value on required argument. --- .../ArgumentAttributes.cs | 57 ++++++++ .../CommandLineArgumentAttribute.cs | 27 ++++ .../Diagnostics.cs | 8 ++ .../ParserGenerator.cs | 135 +++++++----------- .../Properties/Resources.Designer.cs | 18 +++ .../Properties/Resources.resx | 6 + .../Support/GeneratedArgumentProvider.cs | 8 +- src/Samples/TrimTest/Program.cs | 2 +- 8 files changed, 171 insertions(+), 90 deletions(-) create mode 100644 src/Ookii.CommandLine.Generator/ArgumentAttributes.cs create mode 100644 src/Ookii.CommandLine.Generator/CommandLineArgumentAttribute.cs diff --git a/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs new file mode 100644 index 00000000..867b831a --- /dev/null +++ b/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs @@ -0,0 +1,57 @@ +using Microsoft.CodeAnalysis; + +namespace Ookii.CommandLine.Generator; + +internal class ArgumentAttributes +{ + private readonly AttributeData? _commandLineArgumentAttribute; + private readonly AttributeData? _multiValueSeparator; + private readonly AttributeData? _description; + private readonly AttributeData? _valueDescription; + private readonly AttributeData? _allowDuplicateDictionaryKeys; + private readonly AttributeData? _keyValueSeparator; + private readonly AttributeData? _converterAttribute; + private readonly AttributeData? _keyConverterAttribute; + private readonly AttributeData? _valueConverterAttribute; + private readonly List? _aliases; + private readonly List? _shortAliases; + private readonly List? _validators; + + public ArgumentAttributes(IEnumerable attributes, TypeHelper typeHelper, SourceProductionContext context) + { + foreach (var attribute in attributes) + { + if (attribute.CheckType(typeHelper.CommandLineArgumentAttribute, ref _commandLineArgumentAttribute) || + attribute.CheckType(typeHelper.MultiValueSeparatorAttribute, ref _multiValueSeparator) || + attribute.CheckType(typeHelper.DescriptionAttribute, ref _description) || + attribute.CheckType(typeHelper.ValueDescriptionAttribute, ref _valueDescription) || + attribute.CheckType(typeHelper.AllowDuplicateDictionaryKeysAttribute, ref _allowDuplicateDictionaryKeys) || + attribute.CheckType(typeHelper.KeyValueSeparatorAttribute, ref _keyValueSeparator) || + attribute.CheckType(typeHelper.ArgumentConverterAttribute, ref _converterAttribute) || + attribute.CheckType(typeHelper.KeyConverterAttribute, ref _keyConverterAttribute) || + attribute.CheckType(typeHelper.ValueConverterAttribute, ref _valueConverterAttribute) || + attribute.CheckType(typeHelper.AliasAttribute, ref _aliases) || + attribute.CheckType(typeHelper.ShortAliasAttribute, ref _shortAliases) || + attribute.CheckType(typeHelper.ArgumentValidationAttribute, ref _validators)) + { + continue; + } + + context.ReportDiagnostic(Diagnostics.IgnoredAttribute(attribute)); + } + } + + public AttributeData? CommandLineArgument => _commandLineArgumentAttribute; + public AttributeData? MultiValueSeparator => _multiValueSeparator; + public AttributeData? Description => _description; + public AttributeData? ValueDescription => _valueDescription; + public AttributeData? AllowDuplicateDictionaryKeys => _allowDuplicateDictionaryKeys; + public AttributeData? KeyValueSeparator => _keyValueSeparator; + public AttributeData? Converter => _converterAttribute; + public AttributeData? KeyConverter => _keyConverterAttribute; + public AttributeData? ValueConverter => _valueConverterAttribute; + public List? Aliases => _aliases; + public List? ShortAliases => _shortAliases; + public List? Validators => _validators; + +} diff --git a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttribute.cs new file mode 100644 index 00000000..ee128569 --- /dev/null +++ b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttribute.cs @@ -0,0 +1,27 @@ +using Microsoft.CodeAnalysis; + +namespace Ookii.CommandLine.Generator; + +internal class CommandLineArgumentAttributeInfo +{ + public CommandLineArgumentAttributeInfo(AttributeData data) + { + foreach (var named in data.NamedArguments) + { + switch (named.Key) + { + case nameof(IsRequired): + IsRequired = (bool)named.Value.Value!; + break; + + case nameof(DefaultValue): + DefaultValue = named.Value.Value; + break; + } + } + } + + public bool IsRequired { get; } + + public object? DefaultValue { get; } +} diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 3b0bdc01..b2405f69 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -129,6 +129,14 @@ public static Diagnostic CommandAttributeWithoutInterface(INamedTypeSymbol symbo symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic DefaultValueWithRequired(ISymbol symbol) => CreateDiagnostic( + "CLW0005", + nameof(Resources.DefaultValueWithRequiredTitle), + nameof(Resources.DefaultValueWithRequiredMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 2bd979b1..e71d0ad0 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Reflection; using System.Text; +using System.Xml.Linq; namespace Ookii.CommandLine.Generator; @@ -109,19 +110,15 @@ private bool GenerateProvider() _builder.OpenBlock(); _builder.AppendLine("public GeneratedProvider()"); _builder.IncreaseIndent(); - _builder.AppendLine($": base(typeof({_argumentsClass.Name}),"); - _builder.AppendLine($" {attributes.ParseOptions?.CreateInstantiation() ?? "null"},"); - if (attributes.ClassValidators == null) - { - _builder.AppendLine($" null,"); - } - else - { - _builder.AppendLine($" new Ookii.CommandLine.Validation.ClassValidationAttribute[] {{ {string.Join(", ", attributes.ClassValidators.Select(v => v.CreateInstantiation()))} }},"); - } - - _builder.AppendLine($" {attributes.ApplicationFriendlyName?.CreateInstantiation() ?? "null"},"); - _builder.AppendLine($" {attributes.Description?.CreateInstantiation() ?? "null"})"); + _builder.AppendLine(": base("); + _builder.IncreaseIndent(); + _builder.AppendLine($"typeof({_argumentsClass.Name})"); + AppendOptionalAttribute(attributes.ParseOptions, "options"); + AppendOptionalAttribute(attributes.ClassValidators, "validators", "Ookii.CommandLine.Validation.ClassValidationAttribute"); + AppendOptionalAttribute(attributes.ApplicationFriendlyName, "friendlyName"); + AppendOptionalAttribute(attributes.Description, "description"); + _builder.DecreaseIndent(); + _builder.AppendLine(")"); _builder.DecreaseIndent(); _builder.AppendLine("{}"); _builder.AppendLine(); @@ -194,41 +191,10 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> return; } - AttributeData? commandLineArgumentAttribute = null; - AttributeData? multiValueSeparator = null; - AttributeData? description = null; - AttributeData? valueDescription = null; - AttributeData? allowDuplicateDictionaryKeys = null; - AttributeData? keyValueSeparator = null; - AttributeData? converterAttribute = null; - AttributeData? keyConverterAttribute = null; - AttributeData? valueConverterAttribute = null; - List? aliases = null; - List? shortAliases = null; - List? validators = null; - foreach (var attribute in member.GetAttributes()) - { - if (attribute.CheckType(_typeHelper.CommandLineArgumentAttribute, ref commandLineArgumentAttribute) || - attribute.CheckType(_typeHelper.MultiValueSeparatorAttribute, ref multiValueSeparator) || - attribute.CheckType(_typeHelper.DescriptionAttribute, ref description) || - attribute.CheckType(_typeHelper.ValueDescriptionAttribute, ref valueDescription) || - attribute.CheckType(_typeHelper.AllowDuplicateDictionaryKeysAttribute, ref allowDuplicateDictionaryKeys) || - attribute.CheckType(_typeHelper.KeyValueSeparatorAttribute, ref keyValueSeparator) || - attribute.CheckType(_typeHelper.ArgumentConverterAttribute, ref converterAttribute) || - attribute.CheckType(_typeHelper.KeyConverterAttribute, ref keyConverterAttribute) || - attribute.CheckType(_typeHelper.ValueConverterAttribute, ref valueConverterAttribute) || - attribute.CheckType(_typeHelper.AliasAttribute, ref aliases) || - attribute.CheckType(_typeHelper.ShortAliasAttribute, ref shortAliases) || - attribute.CheckType(_typeHelper.ArgumentValidationAttribute, ref validators)) - { - continue; - } - - _context.ReportDiagnostic(Diagnostics.IgnoredAttribute(attribute)); - } + var attributes = new ArgumentAttributes(member.GetAttributes(), _typeHelper, _context); // Check if it is an attribute. - if (commandLineArgumentAttribute == null) + if (attributes.CommandLineArgument == null) { return; } @@ -286,9 +252,9 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> var namedElementTypeWithNullable = elementTypeWithNullable as INamedTypeSymbol; ITypeSymbol? keyType = null; ITypeSymbol? valueType = null; - if (keyValueSeparator != null) + if (attributes.KeyValueSeparator != null) { - _builder.AppendLine($"var keyValueSeparatorAttribute{member.Name} = {keyValueSeparator.CreateInstantiation()};"); + _builder.AppendLine($"var keyValueSeparatorAttribute{member.Name} = {attributes.KeyValueSeparator.CreateInstantiation()};"); } var kind = "Ookii.CommandLine.ArgumentKind.SingleValue"; @@ -312,23 +278,23 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> var rawValueType = namedElementTypeWithNullable.TypeArguments[1]; allowsNull = rawValueType.AllowsNull(); valueType = rawValueType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - if (converterAttribute == null) + if (attributes.Converter == null) { - var keyConverter = DetermineConverter(keyType.GetUnderlyingType(), keyConverterAttribute, keyType.IsNullableValueType()); + var keyConverter = DetermineConverter(keyType.GetUnderlyingType(), attributes.KeyConverter, keyType.IsNullableValueType()); if (keyConverter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); return; } - var valueConverter = DetermineConverter(valueType.GetUnderlyingType(), valueConverterAttribute, valueType.IsNullableValueType()); + var valueConverter = DetermineConverter(valueType.GetUnderlyingType(), attributes.ValueConverter, valueType.IsNullableValueType()); if (valueConverter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); return; } - var separator = keyValueSeparator == null + var separator = attributes.KeyValueSeparator == null ? "null" : $"keyValueSeparatorAttribute{member.Name}.Separator"; @@ -362,7 +328,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> } var elementType = namedElementTypeWithNullable?.GetUnderlyingType() ?? elementTypeWithNullable; - converter ??= DetermineConverter(elementType, converterAttribute, ((INamedTypeSymbol)elementTypeWithNullable).IsNullableValueType()); + converter ??= DetermineConverter(elementType, attributes.Converter, ((INamedTypeSymbol)elementTypeWithNullable).IsNullableValueType()); if (converter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, elementType)); @@ -380,7 +346,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.AppendLine($", elementType: typeof({elementType.ToDisplayString()})"); _builder.AppendLine($", memberName: \"{member.Name}\""); _builder.AppendLine($", kind: {kind}"); - _builder.AppendLine($", attribute: {commandLineArgumentAttribute.CreateInstantiation()}"); + _builder.AppendLine($", attribute: {attributes.CommandLineArgument.CreateInstantiation()}"); _builder.AppendLine($", converter: {converter}"); _builder.AppendLine($", allowsNull: {(allowsNull.ToCSharpString())}"); if (keyType != null) @@ -393,46 +359,22 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.AppendLine($", valueType: typeof({valueType.ToDisplayString()})"); } - if (multiValueSeparator != null) - { - _builder.AppendLine($", multiValueSeparatorAttribute: {multiValueSeparator.CreateInstantiation()}"); - } - - if (description != null) - { - _builder.AppendLine($", descriptionAttribute: {description.CreateInstantiation()}"); - } - - if (valueDescription != null) - { - _builder.AppendLine($", valueDescriptionAttribute: {valueDescription.CreateInstantiation()}"); - } - - if (allowDuplicateDictionaryKeys != null) + AppendOptionalAttribute(attributes.MultiValueSeparator, "multiValueSeparatorAttribute"); + AppendOptionalAttribute(attributes.Description, "descriptionAttribute"); + AppendOptionalAttribute(attributes.ValueDescription, "valueDescriptionAttribute"); + if (attributes.AllowDuplicateDictionaryKeys != null) { _builder.AppendLine(", allowDuplicateDictionaryKeys: true"); } - if (keyValueSeparator != null) + if (attributes.KeyValueSeparator != null) { _builder.AppendLine($", keyValueSeparatorAttribute: keyValueSeparatorAttribute{member.Name}"); } - if (aliases != null) - { - _builder.AppendLine($", aliasAttributes: new Ookii.CommandLine.AliasAttribute[] {{ {string.Join(", ", aliases.Select(a => a.CreateInstantiation()))} }}"); - } - - if (shortAliases != null) - { - _builder.AppendLine($", shortAliasAttributes: new Ookii.CommandLine.ShortAliasAttribute[] {{ {string.Join(", ", shortAliases.Select(a => a.CreateInstantiation()))} }}"); - } - - if (validators != null) - { - _builder.AppendLine($", validationAttributes: new Ookii.CommandLine.Validation.ArgumentValidationAttribute[] {{ {string.Join(", ", validators.Select(a => a.CreateInstantiation()))} }}"); - } - + 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) @@ -442,6 +384,11 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.AppendLine($", getProperty: (target) => (({_argumentsClass.ToDisplayString()})target).{member.Name}"); _builder.AppendLine($", requiredProperty: {property.IsRequired.ToCSharpString()}"); + var argumentInfo = new CommandLineArgumentAttributeInfo(attributes.CommandLineArgument); + if ((property.IsRequired || argumentInfo.IsRequired) && argumentInfo.DefaultValue != null) + { + _context.ReportDiagnostic(Diagnostics.DefaultValueWithRequired(member)); + } } if (methodInfo is MethodArgumentInfo info) @@ -638,4 +585,20 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> return info; } + + private void AppendOptionalAttribute(AttributeData? attribute, string name) + { + if (attribute != null) + { + _builder.AppendLine($", {name}: {attribute.CreateInstantiation()}"); + } + } + + private void AppendOptionalAttribute(List? attributes, string name, string typeName) + { + if (attributes != null) + { + _builder.AppendLine($", {name}: new {typeName}[] {{ {string.Join(", ", attributes.Select(a => a.CreateInstantiation()))} }}"); + } + } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 0b165e17..8b85e6a0 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -150,6 +150,24 @@ internal static string CommandAttributeWithoutInterfaceTitle { } } + /// + /// 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 default value is ignored if the argument is required.. + /// + internal static string DefaultValueWithRequiredTitle { + get { + return ResourceManager.GetString("DefaultValueWithRequiredTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to The command class {0} cannot use the GeneratedParserAttribute class, because it implements the ICommandWithCustomParsing interface.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index ed1e838b..ff491e9e 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -147,6 +147,12 @@ The command line arguments class has the CommandAttribute but does not implement ICommand. + + The default value of the argument defined by {0} is ignored because the argument is required. + + + The default value is ignored if the argument is required. + The command class {0} cannot use the GeneratedParserAttribute class, because it implements the ICommandWithCustomParsing interface. diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs index 0f2f98e6..5a788bcb 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -34,9 +34,11 @@ public abstract class GeneratedArgumentProvider : ArgumentProvider /// The for the arguments type, or if /// there is none. /// - protected GeneratedArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, - IEnumerable? validators, ApplicationFriendlyNameAttribute? friendlyName, - DescriptionAttribute? description) + protected GeneratedArgumentProvider(Type argumentsType, + ParseOptionsAttribute? options = null, + IEnumerable? validators = null, + ApplicationFriendlyNameAttribute? friendlyName = null, + DescriptionAttribute? description = null) : base(argumentsType, options, validators) { _friendlyNameAttribute = friendlyName; diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 00583b34..184c2e80 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -37,7 +37,7 @@ partial class TestProvider { } [Command] partial class Arguments : ICommand { - [CommandLineArgument] + [CommandLineArgument(DefaultValue = 5)] [Description("Test argument")] [Alias("t")] [ValidateNotEmpty] From 085a6e8afd39bd1cff39b1f7339d0c433cc93270 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 1 May 2023 18:01:10 -0700 Subject: [PATCH 055/234] Diagnostics for default value with multi-value and method arguments. --- .../Diagnostics.cs | 20 +++++++++++-- .../ParserGenerator.cs | 21 +++++++++++-- .../Properties/Resources.Designer.cs | 30 +++++++++++++++---- .../Properties/Resources.resx | 12 ++++++-- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 5 ++-- 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index b2405f69..001cb08e 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -131,12 +131,28 @@ public static Diagnostic CommandAttributeWithoutInterface(INamedTypeSymbol symbo public static Diagnostic DefaultValueWithRequired(ISymbol symbol) => CreateDiagnostic( "CLW0005", - nameof(Resources.DefaultValueWithRequiredTitle), + nameof(Resources.DefaultValueIgnoredTitle), nameof(Resources.DefaultValueWithRequiredMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); - + + public static Diagnostic DefaultValueWithMultiValue(ISymbol symbol) => CreateDiagnostic( + "CLW0005", // Deliberately the same as above. + nameof(Resources.DefaultValueIgnoredTitle), + nameof(Resources.DefaultValueWithMultiValueMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + + public static Diagnostic DefaultValueWithMethod(ISymbol symbol) => CreateDiagnostic( + "CLW0005", // Deliberately the same as above. + nameof(Resources.DefaultValueIgnoredTitle), + nameof(Resources.DefaultValueWithMethodMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index e71d0ad0..987893ea 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -199,6 +199,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> return; } + var argumentInfo = new CommandLineArgumentAttributeInfo(attributes.CommandLineArgument); ITypeSymbol originalArgumentType; MethodArgumentInfo? methodInfo = null; var property = member as IPropertySymbol; @@ -257,6 +258,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.AppendLine($"var keyValueSeparatorAttribute{member.Name} = {attributes.KeyValueSeparator.CreateInstantiation()};"); } + var isMultiValue = false; var kind = "Ookii.CommandLine.ArgumentKind.SingleValue"; string? converter = null; if (property != null) @@ -271,6 +273,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> { Debug.Assert(multiValueElementType != null); kind = "Ookii.CommandLine.ArgumentKind.Dictionary"; + isMultiValue = true; elementTypeWithNullable = multiValueElementType!; // KeyValuePair is guaranteed a named type. namedElementTypeWithNullable = (INamedTypeSymbol)elementTypeWithNullable; @@ -305,6 +308,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> { Debug.Assert(multiValueElementType != null); kind = "Ookii.CommandLine.ArgumentKind.MultiValue"; + isMultiValue = true; allowsNull = multiValueElementType!.AllowsNull(); elementTypeWithNullable = multiValueElementType!.WithNullableAnnotation(NullableAnnotation.NotAnnotated); namedElementTypeWithNullable = elementTypeWithNullable as INamedTypeSymbol; @@ -384,10 +388,16 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.AppendLine($", getProperty: (target) => (({_argumentsClass.ToDisplayString()})target).{member.Name}"); _builder.AppendLine($", requiredProperty: {property.IsRequired.ToCSharpString()}"); - var argumentInfo = new CommandLineArgumentAttributeInfo(attributes.CommandLineArgument); - if ((property.IsRequired || argumentInfo.IsRequired) && argumentInfo.DefaultValue != null) + if (argumentInfo.DefaultValue != null) { - _context.ReportDiagnostic(Diagnostics.DefaultValueWithRequired(member)); + if (isMultiValue) + { + _context.ReportDiagnostic(Diagnostics.DefaultValueWithMultiValue(member)); + } + else if (property.IsRequired || argumentInfo.IsRequired) + { + _context.ReportDiagnostic(Diagnostics.DefaultValueWithRequired(member)); + } } } @@ -418,6 +428,11 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> { _builder.AppendLine($", callMethod: (value, parser) => {{ {_argumentsClass.ToDisplayString()}.{member.Name}({arguments}); return true; }}"); } + + if (argumentInfo.DefaultValue != null) + { + _context.ReportDiagnostic(Diagnostics.DefaultValueWithMethod(member)); + } } _builder.DecreaseIndent(); diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 8b85e6a0..f7de04be 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -151,20 +151,38 @@ internal static string CommandAttributeWithoutInterfaceTitle { } /// - /// Looks up a localized string similar to The default value of the argument defined by {0} is ignored because the argument is required.. + /// 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 DefaultValueWithRequiredMessageFormat { + internal static string DefaultValueIgnoredTitle { get { - return ResourceManager.GetString("DefaultValueWithRequiredMessageFormat", resourceCulture); + 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 is ignored if the argument is required.. + /// 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 DefaultValueWithRequiredTitle { + internal static string DefaultValueWithMultiValueMessageFormat { get { - return ResourceManager.GetString("DefaultValueWithRequiredTitle", resourceCulture); + 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); } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index ff491e9e..cb7236e8 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -147,12 +147,18 @@ The command line arguments class has the CommandAttribute but does not implement ICommand. + + 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 default value is ignored if the argument is required. - The command class {0} cannot use the GeneratedParserAttribute class, because it implements the ICommandWithCustomParsing interface. diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index d8b49c2d..2e94329c 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -8,9 +8,8 @@ using System.IO; using System.Net; -// We deliberately have some properties and methods that don't generate arguments, so disable the -// warnings for them from the generator. -#pragma warning disable CLW0002,CLW0003 +// We deliberately have some properties and methods that cause warnings, so disable those. +#pragma warning disable CLW0002,CLW0003,CLW0005 namespace Ookii.CommandLine.Tests; From 53d9a1ad27b59e07bcf3661f86fa06e4bab5bc84 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 1 May 2023 18:05:28 -0700 Subject: [PATCH 056/234] Optimize base class check for sealed classes. --- src/Ookii.CommandLine.Generator/Extensions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 92447aaf..2794e09b 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -24,6 +24,12 @@ public static bool DerivesFrom(this ITypeSymbol symbol, ITypeSymbol? baseClass) return true; } + // No point checking base classes if the type we're looking for is sealed. + if (baseClass.IsSealed) + { + break; + } + current = current.BaseType; } From ef94482aae29709682d3609c72a212f078003934 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 1 May 2023 18:08:49 -0700 Subject: [PATCH 057/234] Fix possibly incorrect cast. --- src/Ookii.CommandLine.Generator/ParserGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 987893ea..419000e2 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -332,7 +332,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> } var elementType = namedElementTypeWithNullable?.GetUnderlyingType() ?? elementTypeWithNullable; - converter ??= DetermineConverter(elementType, attributes.Converter, ((INamedTypeSymbol)elementTypeWithNullable).IsNullableValueType()); + converter ??= DetermineConverter(elementType, attributes.Converter, elementTypeWithNullable.IsNullableValueType()); if (converter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, elementType)); From 119ff9aaf15cae0a68fc74d79762af239e7365d0 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 2 May 2023 14:27:25 -0700 Subject: [PATCH 058/234] Throw exception when reflection is used and GeneratedParserAttribute is present. --- .../ArgumentValidatorTest.cs | 8 + .../CommandLineParserNullableTest.cs | 8 + .../CommandLineParserTest.cs | 8 + .../KeyValuePairConverterTest.cs | 8 + .../Ookii.CommandLine.Tests.csproj | 3 + src/Ookii.CommandLine.Tests/SubCommandTest.cs | 8 + src/Ookii.CommandLine.Tests/ookii.public | Bin 0 -> 160 bytes src/Ookii.CommandLine.Tests/ookii.snk | Bin 0 -> 596 bytes src/Ookii.CommandLine/CommandLineParser.cs | 79 +- .../CommandLineParserGeneric.cs | 26 +- .../Ookii.CommandLine.csproj | 4 + src/Ookii.CommandLine/ParseOptions.cs | 1035 +++++++++-------- .../Properties/Resources.Designer.cs | 9 + .../Properties/Resources.resx | 3 + 14 files changed, 686 insertions(+), 513 deletions(-) create mode 100644 src/Ookii.CommandLine.Tests/ookii.public create mode 100644 src/Ookii.CommandLine.Tests/ookii.snk diff --git a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs index 582583b6..551811e3 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs @@ -14,6 +14,14 @@ public class ArgumentValidatorTest CommandLineParser _parser; CommandLineArgument _argument; + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) + { + // Avoid exception when testing reflection on argument types that also have the + // GeneratedParseAttribute set. + ParseOptions.AllowReflectionWithGeneratedParserDefault = true; + } + [TestInitialize] public void Initialize() { diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs index 2b24f5ed..0e0769fd 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -16,6 +16,14 @@ namespace Ookii.CommandLine.Tests [TestClass] public class CommandLineParserNullableTest { + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) + { + // Avoid exception when testing reflection on argument types that also have the + // GeneratedParseAttribute set. + ParseOptions.AllowReflectionWithGeneratedParserDefault = true; + } + [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] public void TestAllowNull(ProviderKind kind) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index edbfd691..2ab7bf92 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -18,6 +18,14 @@ namespace Ookii.CommandLine.Tests [TestClass()] public partial class CommandLineParserTest { + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) + { + // Avoid exception when testing reflection on argument types that also have the + // GeneratedParseAttribute set. + ParseOptions.AllowReflectionWithGeneratedParserDefault = true; + } + [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] public void ConstructorEmptyArgumentsTest(ProviderKind kind) diff --git a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs index 2ec04866..2120737b 100644 --- a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs +++ b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs @@ -8,6 +8,14 @@ namespace Ookii.CommandLine.Tests [TestClass] public class KeyValuePairConverterTest { + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) + { + // Avoid exception when testing reflection on argument types that also have the + // GeneratedParseAttribute set. + ParseOptions.AllowReflectionWithGeneratedParserDefault = true; + } + // Needed because SpanParsableConverter only exists on .Net 7. private class IntConverter : ArgumentConverter { diff --git a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj index d37e1888..4211ac00 100644 --- a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj +++ b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj @@ -7,6 +7,9 @@ false 11.0 true + true + ookii.snk + false diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 38299317..0800d886 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -16,6 +16,14 @@ public class SubCommandTest { private static readonly Assembly _commandAssembly = Assembly.GetExecutingAssembly(); + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) + { + // Avoid exception when testing reflection on argument types that also have the + // GeneratedParseAttribute set. + ParseOptions.AllowReflectionWithGeneratedParserDefault = true; + } + [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] public void GetCommandsTest(ProviderKind kind) diff --git a/src/Ookii.CommandLine.Tests/ookii.public b/src/Ookii.CommandLine.Tests/ookii.public new file mode 100644 index 0000000000000000000000000000000000000000..67c4e827b5c53d2b87311f5e2faedeb50b010a9d GIT binary patch literal 160 zcmV;R0AK$ABme*efB*oL000060ssI2Bme+XQ$aBR1ONa50097h;jblssCt@a0%K~( z>Kgj%%NuoDbI&o}u*b<8CIqceR|Pfm>E`4$C30P=)yKgj%%NuoDbI&o} zu*b<8CIqceR|Pfm>E`4$C30P=)yO?!ABF=4 z-f_D#=T22nU5(%Yp}2`^x_>?Qe%nY`+?w_$mcIQ`YZ2u>?%*F-5wY;;ud`leSUaS< zN-yb7um7u$LUCIwSRxp+E7!>{kpRgZfuYSIv)NMhV1t6UHK-76f$?6f#(Wj{GZc0g zbLQ@qqCyz!2_vZad=byO2@z<>t93lsv(kYFB{N>U_Dm=NDR3=&yxw<7&f{R($=oDf zrvOl~R$+w0;}wBh=bXpujy(RZN@>j+A0e^0JEf9Ad=9zHe7%-@H}kSyW`>#Eanow_ z-#71bK+XbZo~d4NwiTB!Di!2 zRF3JjQXIPhRE?r5w|iM6l_&WD118pH<0ynJCn=HY+Cdj+?Du+N79N80+ryv%@PBlZ zq6SLd>_PcyH9|hee}>u2DqK?rYA9NA5wk@Rf*W41@1q`{9eo$>4{`LaN!m)DFnlJf iDg@evY}P48mDZW@#ET&MA_j(Cq3Xw#L3spw?UyBgz#jMj literal 0 HcmV?d00001 diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index d6ee11f9..4a5363e4 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -286,8 +286,24 @@ private struct PrefixInfo /// because it violates one of the rules concerning argument names or positions, or has an argument type that cannot /// be parsed. /// + /// + /// The type indicated by has the + /// attribute applied. Use the generated static CreateParser() or Parse() + /// methods on the arguments type to access the generated parser. For subcommands, use a + /// command provider with the attribute to + /// create a that will use generated parsers for subcommands. Set + /// the property to + /// to disable this exception. + /// /// /// + /// This constructor uses reflection to determine the arguments defined by the type indicated + /// by at runtime. To determine the arguments at compile + /// time instead, apply the to the arguments type + /// and use the generated static CreateParser() or Parse() methods on that type + /// 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. @@ -297,11 +313,6 @@ private struct PrefixInfo /// 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. - /// /// #if NET6_0_OR_GREATER [RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] @@ -331,6 +342,16 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) /// positions, or has an argument type that cannot /// be parsed. /// + /// + /// The provider uses , but the type indicated by the + /// property has the + /// attribute applied. Use the generated static CreateParser() or Parse() + /// methods on the arguments type to access the generated parser. For subcommands, use a + /// command provider with the attribute to + /// create a that will use generated parsers for subcommands. Set + /// the property to + /// to disable this exception. + /// /// /// /// If the parameter is not , the @@ -342,16 +363,18 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) /// 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(ArgumentProvider provider, ParseOptions? options = null) { _provider = provider ?? throw new ArgumentNullException(nameof(provider)); _parseOptions = options ?? new(); + if (provider.Kind == ProviderKind.Reflection && + !_parseOptions.AllowReflectionWithGeneratedParser && + Attribute.IsDefined(provider.ArgumentsType, typeof(GeneratedParserAttribute))) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + Properties.Resources.ReflectionWithGeneratedParserFormat, provider.ArgumentsType.FullName)); + } var optionsAttribute = _provider.OptionsAttribute; if (optionsAttribute != null) @@ -1000,6 +1023,23 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// error occurred, or argument parsing was canceled by the /// property or a method argument that returned . /// + /// + /// + /// + /// + /// The type indicated by has the + /// attribute applied. Use the generated static CreateParser() or Parse() + /// methods on the arguments type to access the generated parser. For subcommands, use a + /// command provider with the attribute to + /// create a that will use generated parsers for subcommands. Set + /// the property to + /// to disable this exception. + /// + /// + /// 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 , @@ -1025,6 +1065,13 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// instance of the class and call its /// method. /// + /// + /// This method uses reflection to determine the arguments defined by the type indicated + /// by at runtime. To determine the arguments at compile + /// time instead, apply the to the arguments type + /// and use the generated static CreateParser() or Parse() methods on that type + /// instead. + /// /// #if NET6_0_OR_GREATER [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] @@ -1056,6 +1103,12 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// does not fall within the bounds of . /// + /// + /// + /// + /// + /// + /// /// /// /// @@ -1091,6 +1144,12 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// + /// + /// + /// + /// + /// + /// /// /// /// diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index e1f2c1a1..6c0e818b 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -1,4 +1,5 @@ -using Ookii.CommandLine.Support; +using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Support; using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -37,6 +38,15 @@ public class CommandLineParser : CommandLineParser /// 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 type indicated by has the + /// attribute applied. Use the generated static CreateParser() or Parse() + /// methods on the arguments type to access the generated parser. For subcommands, use a + /// command provider with the attribute to + /// create a that will use generated parsers for subcommands. Set + /// the property to + /// to disable this exception. + /// /// /// /// @@ -63,6 +73,20 @@ public CommandLineParser(ParseOptions? options = null) /// 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 . + /// + /// + /// The provider uses , but the type indicated by the + /// property has the + /// attribute applied. Use the generated static CreateParser() or Parse() + /// methods on the arguments type to access the generated parser. For subcommands, use a + /// command provider with the attribute to + /// create a that will use generated parsers for subcommands. Set + /// the property to + /// to disable this exception. + /// /// /// /// diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index 113463a6..de88bacf 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -48,6 +48,10 @@ + + + + True diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index e9d4d15c..2b23bfc8 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,541 +6,573 @@ using System.Globalization; using System.IO; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Provides options for the +/// method and the constructor. +/// +/// +/// +/// 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 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; } + /// - /// Provides options for the - /// method and the constructor. + /// 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 + /// . + /// /// /// - /// 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. + /// If not , this property overrides the value of the + /// property. /// /// - public class ParseOptions - { - private UsageWriter? _usageWriter; - private LocalizedStringProvider? _stringProvider; + /// + public ParsingMode? Mode { get; set; } - /// - /// 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 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 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 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 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. + /// + /// + /// - /// - /// 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 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; } + public IEnumerable? ArgumentNamePrefixes { 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. - /// - /// - /// + /// + /// 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; } - public IEnumerable? ArgumentNamePrefixes { get; set; } + /// + /// 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 => _stringProvider ??= new LocalizedStringProvider(); + set => _stringProvider = value; + } - /// - /// 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 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 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 => _stringProvider ??= new LocalizedStringProvider(); - set => _stringProvider = value; - } + /// + /// 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 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 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 or the + /// property. + /// + /// + /// If not , this property overrides the + /// property. + /// + /// + public NameTransform? ValueDescriptionTransform { 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 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 whether the class + /// will allow the use of reflection with an arguments class that also has the + /// attribute. + /// + /// + /// to allow the use of reflection when the arguments class has the + /// attribute; otherwise, . The + /// default value is . + /// + /// + /// + /// When this property is (the default), the + /// constructor, the + /// constructor, and the static methods + /// will throw an exception if they are used with an arguments type that has the + /// applied. This is done to avoid the situation + /// where the user wanted to use a generated parser, but is accidentally using reflection + /// instead. + /// + /// + /// If you need to support using arguments classes that may have the + /// attribute, for example when using commands from an external assembly that you don't + /// control, set this property to to avoid the exception. Note that + /// you will not get the performance benefits of the generated code in this case. + /// + /// + public bool AllowReflectionWithGeneratedParser { get; set; } = AllowReflectionWithGeneratedParserDefault; - /// - /// 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 or the - /// property. - /// - /// - /// If not , this property overrides the - /// property. - /// - /// - public NameTransform? ValueDescriptionTransform { get; set; } + // Used by the tests so we can get coverage of the default options path while not causing + // exceptions. + internal static bool AllowReflectionWithGeneratedParserDefault { get; set; } - /// - /// Gets or sets the to use to create usage help. - /// - /// - /// An instance of the class. - /// + /// + /// Gets or sets the to use to create usage help. + /// + /// + /// An instance of the class. + /// #if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - [AllowNull] + [AllowNull] #endif - public UsageWriter UsageWriter - { - get => _usageWriter ??= new UsageWriter(); - set => _usageWriter = value; - } + 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) + /// + /// 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 (attribute == null) - { - 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; - NameValueSeparator ??= attribute.NameValueSeparator; - AutoHelpArgument ??= attribute.AutoHelpArgument; - AutoVersionArgument ??= attribute.AutoVersionArgument; - ValueDescriptionTransform ??= attribute.ValueDescriptionTransform; + throw new ArgumentNullException(nameof(attribute)); } - internal VirtualTerminalSupport? EnableErrorColor() - { - if (Error == null && UseErrorColor == null) - { - var support = VirtualTerminal.EnableColor(StandardStream.Error); - UseErrorColor = support.IsSupported; - return support; - } + Mode ??= attribute.Mode; + ArgumentNameTransform ??= attribute.ArgumentNameTransform; + ArgumentNamePrefixes ??= attribute.ArgumentNamePrefixes; + LongArgumentNamePrefix ??= attribute.LongArgumentNamePrefix; + ArgumentNameComparison ??= attribute.GetStringComparison(); + DuplicateArguments ??= attribute.DuplicateArguments; + AllowWhiteSpaceValueSeparator ??= attribute.AllowWhiteSpaceValueSeparator; + NameValueSeparator ??= attribute.NameValueSeparator; + AutoHelpArgument ??= attribute.AutoHelpArgument; + AutoVersionArgument ??= attribute.AutoVersionArgument; + 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/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 4244c781..5ffd19c3 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -519,6 +519,15 @@ internal static string PropertyIsReadOnlyFormat { } } + /// + /// Looks up a localized string similar to The arguments type {0} has the GeneratedParserAttribute applied, but reflection is being used to create a parser for it. Use the generated {0}.CreateParser() or {0}.Parse() methods to use the generated parser. For subcommands, use the GeneratedCommandProviderAttribute.. + /// + internal static string ReflectionWithGeneratedParserFormat { + get { + return ResourceManager.GetString("ReflectionWithGeneratedParserFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to RequiresAnyAttribute requires at least two arguments; use CommandLineArgumentAttribute.IsRequired to make a single argument required.. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index 9df12732..a7384222 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -399,4 +399,7 @@ The command does not use custom parsing. + + The arguments type {0} has the GeneratedParserAttribute applied, but reflection is being used to create a parser for it. Use the generated {0}.CreateParser() or {0}.Parse() methods to use the generated parser. For subcommands, use the GeneratedCommandProviderAttribute. + \ No newline at end of file From acdd7fedb6088fe951517db7fc1e658f77d122ea Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 2 May 2023 17:33:43 -0700 Subject: [PATCH 059/234] Diagnostic for IsRequired on required property. --- ....cs => CommandLineArgumentAttributeInfo.cs} | 3 +++ src/Ookii.CommandLine.Generator/Diagnostics.cs | 8 ++++++++ .../ParserGenerator.cs | 5 +++++ .../Properties/Resources.Designer.cs | 18 ++++++++++++++++++ .../Properties/Resources.resx | 6 ++++++ src/Samples/TrimTest/Program.cs | 2 +- 6 files changed, 41 insertions(+), 1 deletion(-) rename src/Ookii.CommandLine.Generator/{CommandLineArgumentAttribute.cs => CommandLineArgumentAttributeInfo.cs} (85%) diff --git a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs similarity index 85% rename from src/Ookii.CommandLine.Generator/CommandLineArgumentAttribute.cs rename to src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs index ee128569..c027647b 100644 --- a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs @@ -12,6 +12,7 @@ public CommandLineArgumentAttributeInfo(AttributeData data) { case nameof(IsRequired): IsRequired = (bool)named.Value.Value!; + HasIsRequired = true; break; case nameof(DefaultValue): @@ -23,5 +24,7 @@ public CommandLineArgumentAttributeInfo(AttributeData data) public bool IsRequired { get; } + public bool HasIsRequired { get; } + public object? DefaultValue { get; } } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 001cb08e..a3c006b0 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -153,6 +153,14 @@ public static Diagnostic DefaultValueWithMethod(ISymbol symbol) => CreateDiagnos symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic IsRequiredWithRequiredProperty(ISymbol symbol) => CreateDiagnostic( + "CLW0006", // Deliberately the same as above. + nameof(Resources.IsRequiredWithRequiredPropertyTitle), + nameof(Resources.IsRequiredWithRequiredPropertyMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 419000e2..fd1ed3d3 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -399,6 +399,11 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _context.ReportDiagnostic(Diagnostics.DefaultValueWithRequired(member)); } } + + if (argumentInfo.HasIsRequired && property.IsRequired) + { + _context.ReportDiagnostic(Diagnostics.IsRequiredWithRequiredProperty(member)); + } } if (methodInfo is MethodArgumentInfo info) diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index f7de04be..ec9e2df8 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -240,6 +240,24 @@ internal static string InvalidMethodSignatureTitle { } } + /// + /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsRequired property is ignored for the required property {0}.. + /// + 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 No command line argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index cb7236e8..d318b402 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -177,6 +177,12 @@ A method command line argument has an invalid signature. + + The CommandLineArgumentAttribute.IsRequired property is ignored for the required property {0}. + + + The CommandLineArgumentAttribute.IsRequired property is ignored for a required property. + No command line argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter. diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 184c2e80..00583b34 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -37,7 +37,7 @@ partial class TestProvider { } [Command] partial class Arguments : ICommand { - [CommandLineArgument(DefaultValue = 5)] + [CommandLineArgument] [Description("Test argument")] [Alias("t")] [ValidateNotEmpty] From 0bc046de25d2117c90a950396115623fad2d2241 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 2 May 2023 17:45:23 -0700 Subject: [PATCH 060/234] Diagnostic for duplicate positions. --- .../CommandLineArgumentAttributeInfo.cs | 11 +++++++++++ src/Ookii.CommandLine.Generator/Diagnostics.cs | 12 +++++++++++- .../ParserGenerator.cs | 14 ++++++++++++++ .../Properties/Resources.Designer.cs | 18 ++++++++++++++++++ .../Properties/Resources.resx | 6 ++++++ .../NullableArgumentTypes.cs | 3 +++ 6 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs index c027647b..005ae052 100644 --- a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs +++ b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs @@ -18,6 +18,15 @@ public CommandLineArgumentAttributeInfo(AttributeData data) case nameof(DefaultValue): DefaultValue = named.Value.Value; break; + + case nameof(Position): + var position = (int)named.Value.Value!; + if (position >= 0) + { + Position = position; + } + + break; } } } @@ -26,5 +35,7 @@ public CommandLineArgumentAttributeInfo(AttributeData data) public bool HasIsRequired { get; } + public int? Position { get; } + public object? DefaultValue { get; } } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index a3c006b0..9fa31bcb 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -154,13 +154,23 @@ public static Diagnostic DefaultValueWithMethod(ISymbol symbol) => CreateDiagnos symbol.ToDisplayString()); public static Diagnostic IsRequiredWithRequiredProperty(ISymbol symbol) => CreateDiagnostic( - "CLW0006", // Deliberately the same as above. + "CLW0006", nameof(Resources.IsRequiredWithRequiredPropertyTitle), nameof(Resources.IsRequiredWithRequiredPropertyMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic DuplicatePosition(ISymbol symbol, string otherName) => CreateDiagnostic( + "CLW0007", + nameof(Resources.DuplicatePositionTitle), + nameof(Resources.DuplicatePositionMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString(), + otherName); + + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index fd1ed3d3..0b9b1cf3 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -28,6 +28,7 @@ private struct MethodArgumentInfo private readonly SourceBuilder _builder; private readonly ConverterGenerator _converterGenerator; private readonly CommandGenerator _commandGenerator; + private Dictionary? _positions; public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumentsClass, TypeHelper typeHelper, ConverterGenerator converterGenerator, CommandGenerator commandGenerator) { @@ -442,6 +443,19 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.DecreaseIndent(); _builder.AppendLine(");"); + + if (argumentInfo.Position is int position) + { + _positions ??= new(); + if (_positions.TryGetValue(position, out string name)) + { + _context.ReportDiagnostic(Diagnostics.DuplicatePosition(member, name)); + } + else + { + _positions.Add(position, member.Name); + } + } } private (ITypeSymbol?, INamedTypeSymbol?, ITypeSymbol?)? DetermineMultiValueType(IPropertySymbol property, ITypeSymbol argumentType) diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index ec9e2df8..948b683d 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -186,6 +186,24 @@ internal static string DefaultValueWithRequiredMessageFormat { } } + /// + /// 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.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index d318b402..76ca6515 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -159,6 +159,12 @@ 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. diff --git a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs index 257f9b58..b63bd82b 100644 --- a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs @@ -7,6 +7,9 @@ namespace Ookii.CommandLine.Tests; +// We deliberately have some properties and methods that cause warnings, so disable those. +#pragma warning disable CLW0006 + class NullReturningStringConverter : ArgumentConverter { public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) From 196d3c6c31018b5dfb76572a458ccb040993d73c Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 2 May 2023 17:57:42 -0700 Subject: [PATCH 061/234] Diagnostics for ignored Alias/ShortAlias. --- .../CommandLineArgumentAttributeInfo.cs | 20 +++++++++++ .../Diagnostics.cs | 15 ++++++++ .../ParserGenerator.cs | 11 +++++- .../Properties/Resources.Designer.cs | 36 +++++++++++++++++++ .../Properties/Resources.resx | 12 +++++++ src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 2 +- 6 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs index 005ae052..73628eb9 100644 --- a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs +++ b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs @@ -4,6 +4,8 @@ namespace Ookii.CommandLine.Generator; internal class CommandLineArgumentAttributeInfo { + private readonly bool _isShort; + public CommandLineArgumentAttributeInfo(AttributeData data) { foreach (var named in data.NamedArguments) @@ -27,6 +29,18 @@ public CommandLineArgumentAttributeInfo(AttributeData data) } break; + + case nameof(IsShort): + _isShort = (bool)named.Value.Value!; + break; + + case nameof(ShortName): + ShortName = (char)named.Value.Value!; + break; + + case nameof(IsLong): + IsLong = (bool)named.Value.Value!; + break; } } } @@ -38,4 +52,10 @@ public CommandLineArgumentAttributeInfo(AttributeData data) public int? Position { get; } public object? DefaultValue { get; } + + public bool IsShort => _isShort || ShortName != '\0'; + + public char ShortName { get; } + + public bool IsLong { get; } = true; } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 9fa31bcb..01976e49 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -170,6 +170,21 @@ public static Diagnostic DuplicatePosition(ISymbol symbol, string otherName) => symbol.ToDisplayString(), otherName); + public static Diagnostic ShortAliasWithoutShortName(ISymbol symbol) => CreateDiagnostic( + "CLW0008", + nameof(Resources.ShortAliasWithoutShortNameTitle), + nameof(Resources.ShortAliasWithoutShortNameMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + + public static Diagnostic AliasWithoutLongName(ISymbol symbol) => CreateDiagnostic( + "CLW0009", + nameof(Resources.AliasWithoutLongNameTitle), + nameof(Resources.AliasWithoutLongNameMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 0b9b1cf3..0c03d4ee 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -443,7 +443,6 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.DecreaseIndent(); _builder.AppendLine(");"); - if (argumentInfo.Position is int position) { _positions ??= new(); @@ -456,6 +455,16 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _positions.Add(position, member.Name); } } + + if (!argumentInfo.IsShort && attributes.ShortAliases != null) + { + _context.ReportDiagnostic(Diagnostics.ShortAliasWithoutShortName(member)); + } + + if (!argumentInfo.IsLong && attributes.Aliases != null) + { + _context.ReportDiagnostic(Diagnostics.AliasWithoutLongName(member)); + } } private (ITypeSymbol?, INamedTypeSymbol?, ITypeSymbol?)? DetermineMultiValueType(IPropertySymbol property, ITypeSymbol argumentType) diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 948b683d..51bcfc5b 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -60,6 +60,24 @@ internal Resources() { } } + /// + /// 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 arguments class {0} may not be a generic class when the GeneratedParserAttribute is used.. /// @@ -366,6 +384,24 @@ internal static string PropertyIsReadOnlyTitle { } } + /// + /// 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 attribute {0} is unknown and will be ignored by the GeneratedParserAttribute.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 76ca6515..c0b7b911 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -117,6 +117,12 @@ 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 arguments class {0} may not be a generic class when the GeneratedParserAttribute is used. @@ -219,6 +225,12 @@ 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 attribute {0} is unknown and will be ignored by the GeneratedParserAttribute. diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 2e94329c..2ab6461e 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -9,7 +9,7 @@ using System.Net; // We deliberately have some properties and methods that cause warnings, so disable those. -#pragma warning disable CLW0002,CLW0003,CLW0005 +#pragma warning disable CLW0002,CLW0003,CLW0005,CLW0008,CLW0009 namespace Ookii.CommandLine.Tests; From 54bf9053156eda598b2c39a821c41f97639317b7 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 2 May 2023 18:04:56 -0700 Subject: [PATCH 062/234] Diagnostic for hidden positional arguments. --- .../CommandLineArgumentAttributeInfo.cs | 6 ++++++ src/Ookii.CommandLine.Generator/Diagnostics.cs | 8 ++++++++ .../ParserGenerator.cs | 5 +++++ .../Properties/Resources.Designer.cs | 18 ++++++++++++++++++ .../Properties/Resources.resx | 6 ++++++ src/Samples/TrimTest/Program.cs | 2 +- 6 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs index 73628eb9..990d7e7f 100644 --- a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs +++ b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs @@ -41,6 +41,10 @@ public CommandLineArgumentAttributeInfo(AttributeData data) case nameof(IsLong): IsLong = (bool)named.Value.Value!; break; + + case nameof(IsHidden): + IsHidden = (bool)named.Value.Value!; + break; } } } @@ -58,4 +62,6 @@ public CommandLineArgumentAttributeInfo(AttributeData data) public char ShortName { get; } public bool IsLong { get; } = true; + + public bool IsHidden { get; } } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 01976e49..cc9beafc 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -186,6 +186,14 @@ public static Diagnostic AliasWithoutLongName(ISymbol symbol) => CreateDiagnosti symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic IsHiddenWithPositional(ISymbol symbol) => CreateDiagnostic( + "CLW0010", + nameof(Resources.IsHiddenWithPositionalTitle), + nameof(Resources.IsHiddenWithPositionalMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 0c03d4ee..962f9c9d 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -465,6 +465,11 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> { _context.ReportDiagnostic(Diagnostics.AliasWithoutLongName(member)); } + + if (argumentInfo.IsHidden && argumentInfo.Position != null) + { + _context.ReportDiagnostic(Diagnostics.IsHiddenWithPositional(member)); + } } private (ITypeSymbol?, INamedTypeSymbol?, ITypeSymbol?)? DetermineMultiValueType(IPropertySymbol property, ITypeSymbol argumentType) diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 51bcfc5b..baf796d2 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -276,6 +276,24 @@ internal static string InvalidMethodSignatureTitle { } } + /// + /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsHidden property is ignored for the argument defined by {0} because it is positional.. + /// + internal static string IsHiddenWithPositionalMessageFormat { + get { + return ResourceManager.GetString("IsHiddenWithPositionalMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsHidden property is ignored for positional arguments.. + /// + internal static string IsHiddenWithPositionalTitle { + get { + return ResourceManager.GetString("IsHiddenWithPositionalTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsRequired property is ignored for the required property {0}.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index c0b7b911..dc06c54a 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -189,6 +189,12 @@ 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. + + + The CommandLineArgumentAttribute.IsHidden property is ignored for positional arguments. + The CommandLineArgumentAttribute.IsRequired property is ignored for the required property {0}. diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 00583b34..e5a6bf08 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -37,7 +37,7 @@ partial class TestProvider { } [Command] partial class Arguments : ICommand { - [CommandLineArgument] + [CommandLineArgument(Position = 0)] [Description("Test argument")] [Alias("t")] [ValidateNotEmpty] From c0ee4defe0c9a02fc6ef77c9ba4136abd0fabc7d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 3 May 2023 12:45:38 -0700 Subject: [PATCH 063/234] Diagnostic for violating positional argument rules. --- .../Diagnostics.cs | 18 +++++ .../ParserGenerator.cs | 65 +++++++++++++++++++ .../Properties/Resources.Designer.cs | 36 ++++++++++ .../Properties/Resources.resx | 12 ++++ src/Samples/TrimTest/Program.cs | 2 +- 5 files changed, 132 insertions(+), 1 deletion(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index cc9beafc..8235f02a 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -95,6 +95,24 @@ public static Diagnostic GeneratedCustomParsingCommand(INamedTypeSymbol symbol) symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic PositionalArgumentAfterMultiValue(ISymbol symbol, string other) => CreateDiagnostic( + "CL0011", + nameof(Resources.PositionalArgumentAfterMultiValueTitle), + nameof(Resources.PositionalArgumentAfterMultiValueMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString(), + other); + + public static Diagnostic PositionalRequiredArgumentAfterOptional(ISymbol symbol, string other) => CreateDiagnostic( + "CL0012", + nameof(Resources.PositionalRequiredArgumentAfterOptionalTitle), + nameof(Resources.PositionalRequiredArgumentAfterOptionalMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString(), + other); + public static Diagnostic IgnoredAttribute(AttributeData attribute) => CreateDiagnostic( "CLW0001", nameof(Resources.UnknownAttributeTitle), diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 962f9c9d..00844017 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -21,6 +21,14 @@ private struct MethodArgumentInfo public bool HasBooleanReturn { 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; @@ -29,6 +37,7 @@ private struct MethodArgumentInfo private readonly ConverterGenerator _converterGenerator; private readonly CommandGenerator _commandGenerator; private Dictionary? _positions; + private List? _positionalArguments; public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumentsClass, TypeHelper typeHelper, ConverterGenerator converterGenerator, CommandGenerator commandGenerator) { @@ -140,6 +149,11 @@ private bool GenerateProvider() current = current.BaseType; } + if (!VerifyPositionalArgumentRules()) + { + return false; + } + // Makes sure the function compiles if there are no arguments. _builder.AppendLine("yield break;"); _builder.CloseBlock(); // GetArguments() @@ -260,6 +274,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> } var isMultiValue = false; + var isRequired = argumentInfo.IsRequired; var kind = "Ookii.CommandLine.ArgumentKind.SingleValue"; string? converter = null; if (property != null) @@ -323,6 +338,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> if (property.IsRequired) { + isRequired = true; requiredProperties ??= new(); requiredProperties.Add((member.Name, property.Type.ToDisplayString(), notNullAnnotation)); } @@ -454,6 +470,15 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> { _positions.Add(position, member.Name); } + + _positionalArguments ??= new(); + _positionalArguments.Add(new PositionalArgumentInfo() + { + Member = member, + Position = position, + IsRequired = isRequired, + IsMultiValue = isMultiValue + }); } if (!argumentInfo.IsShort && attributes.ShortAliases != null) @@ -649,4 +674,44 @@ private void AppendOptionalAttribute(List? attributes, string nam _builder.AppendLine($", {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; + } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index baf796d2..bf025933 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -384,6 +384,42 @@ internal static string NonRequiredInitOnlyPropertyTitle { } } + /// + /// 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}.{1} must have a public set accessor.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index dc06c54a..196838c5 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -225,6 +225,18 @@ Init accessors may only be used on required properties. + + 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}.{1} must have a public set accessor. diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index e5a6bf08..e67ff941 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -43,7 +43,7 @@ partial class Arguments : ICommand [ValidateNotEmpty] public string? Test { get; set; } - [CommandLineArgument] + [CommandLineArgument(Position = 1)] [ValueDescription("Stuff")] [KeyValueSeparator("==")] [MultiValueSeparator] From 36c4340a0bab903fbc8f135bb5799b4b39b1f308 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 3 May 2023 18:00:55 -0700 Subject: [PATCH 064/234] Customizable namespace for generated converters. --- .../ConverterGenerator.cs | 75 +++++++++++++++---- .../Diagnostics.cs | 8 ++ .../ParserIncrementalGenerator.cs | 2 +- .../Properties/Resources.Designer.cs | 18 +++++ .../Properties/Resources.resx | 6 ++ src/Ookii.CommandLine.Generator/TypeHelper.cs | 2 + .../GeneratedConverterNamespaceAttribute.cs | 42 +++++++++++ 7 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 src/Ookii.CommandLine/Conversion/GeneratedConverterNamespaceAttribute.cs diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 6fe79906..bbf0d745 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -1,5 +1,7 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using System.Text; +using System.Xml.Linq; namespace Ookii.CommandLine.Generator; @@ -14,8 +16,6 @@ private struct ConverterInfo public bool HasCulture { get; set; } public bool UseSpan { get; set; } - public string ConstructorCall => $"new {GeneratedNamespace}.{Name}()"; - public bool IsBetter(ConverterInfo other) { // Prefer Parse over constructor. @@ -43,34 +43,37 @@ public bool IsBetter(ConverterInfo other) #endregion // TODO: Customizable or random namespace? - private const string GeneratedNamespace = "Ookii.CommandLine.Conversion.Generated"; + private const string DefaultGeneratedNamespace = "Ookii.CommandLine.Conversion.Generated"; private const string ConverterSuffix = "Converter"; private readonly INamedTypeSymbol? _readOnlySpanType; private readonly INamedTypeSymbol? _cultureType; private readonly Dictionary _converters = new(SymbolEqualityComparer.Default); - public ConverterGenerator(TypeHelper typeHelper) + public ConverterGenerator(TypeHelper typeHelper, SourceProductionContext context) { _cultureType = typeHelper.CultureInfo; _readOnlySpanType = typeHelper.ReadOnlySpanOfChar; + GeneratedNamespace = GetGeneratedNamespace(typeHelper, context); } + public string GeneratedNamespace { get; } + public string? GetConverter(ITypeSymbol type) { - if (_converters.TryGetValue(type, out var converter)) + if (!_converters.TryGetValue(type, out var converter)) { - return converter.ConstructorCall; - } + var optionalInfo = FindParseMethod(type) ?? FindConstructor(type); + if (optionalInfo is not ConverterInfo info) + { + return null; + } - var optionalInfo = FindParseMethod(type) ?? FindConstructor(type); - if (optionalInfo is not ConverterInfo info) - { - return null; + info.Name = GenerateName(type.ToDisplayString()); + _converters.Add(type, info); + converter = info; } - info.Name = GenerateName(type.ToDisplayString()); - _converters.Add(type, info); - return info.ConstructorCall; + return $"new {GeneratedNamespace}.{converter.Name}()"; } public string? Generate() @@ -199,7 +202,7 @@ private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, Con builder.AppendLine("catch (Ookii.CommandLine.CommandLineArgumentException ex)"); builder.OpenBlock(); // Patch the exception with the argument name. - builder.AppendLine("throw new CommandLineArgumentException(ex.Message, argument.ArgumentName, ex.Category, ex.InnerException);"); + builder.AppendLine("throw new Ookii.CommandLine.CommandLineArgumentException(ex.Message, argument.ArgumentName, ex.Category, ex.InnerException);"); builder.CloseBlock(); // catch builder.AppendLine("catch (System.FormatException)"); builder.OpenBlock(); @@ -219,4 +222,46 @@ private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, Con builder.CloseBlock(); // class builder.AppendLine(); } + + private static string GetGeneratedNamespace(TypeHelper typeHelper, SourceProductionContext context) + { + var attributeType = typeHelper.GeneratedConverterNamespaceAttribute; + if (attributeType == null) + { + return DefaultGeneratedNamespace; + } + + AttributeData? attribute = null; + foreach (var attr in typeHelper.Compilation.Assembly.GetAttributes()) + { + if (attributeType.SymbolEquals(attr.AttributeClass)) + { + attribute = attr; + break; + } + } + + if (attribute == null) + { + return DefaultGeneratedNamespace; + } + + var ns = attribute.ConstructorArguments.FirstOrDefault().Value as string; + if (ns == null) + { + return DefaultGeneratedNamespace; + } + + var elements = ns.Split('.'); + foreach (var element in elements) + { + if (!SyntaxFacts.IsValidIdentifier(element)) + { + context.ReportDiagnostic(Diagnostics.InvalidGeneratedConverterNamespace(ns, attribute)); + return DefaultGeneratedNamespace; + } + } + + return ns; + } } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 8235f02a..9f34fcf4 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -212,6 +212,14 @@ public static Diagnostic IsHiddenWithPositional(ISymbol symbol) => CreateDiagnos symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic InvalidGeneratedConverterNamespace(string ns, AttributeData attribute) => CreateDiagnostic( + "CLW0010", + nameof(Resources.InvalidGeneratedConverterNamespaceTitle), + nameof(Resources.InvalidGeneratedConverterNamespaceMessageFormat), + DiagnosticSeverity.Warning, + attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), + ns); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index 3ed01465..15e2b648 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -42,7 +42,7 @@ private static void Execute(Compilation compilation, ImmutableArray } var typeHelper = new TypeHelper(compilation); - var converterGenerator = new ConverterGenerator(typeHelper); + var converterGenerator = new ConverterGenerator(typeHelper, context); var commandGenerator = new CommandGenerator(typeHelper, context); foreach (var cls in classes) { diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index bf025933..9e628b6c 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -258,6 +258,24 @@ internal static string InvalidArrayRankTitle { } } + /// + /// 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}.{1} does not have a valid signature for a command line argument.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 196838c5..9b76a30b 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -183,6 +183,12 @@ A multi-value command line argument defined by an array properties must have an array rank of one. + + 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}.{1} does not have a valid signature for a command line argument. diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index 14c3e8c0..52108069 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -75,6 +75,8 @@ public TypeHelper(Compilation compilation) 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? GeneratedCommandProviderAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.GeneratedCommandProviderAttribute"); diff --git a/src/Ookii.CommandLine/Conversion/GeneratedConverterNamespaceAttribute.cs b/src/Ookii.CommandLine/Conversion/GeneratedConverterNamespaceAttribute.cs new file mode 100644 index 00000000..7f5648f6 --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/GeneratedConverterNamespaceAttribute.cs @@ -0,0 +1,42 @@ +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. + public GeneratedConverterNamespaceAttribute(string @namespace) + { + Namespace = @namespace; + } + + /// + /// Gets the namespace to use for generated argument converters. + /// + /// + /// The full name of the namespace. + /// + public string Namespace { get; } +} From 6f15cfee2ec171434710cb9be6820f50bd189d2f Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 4 May 2023 15:46:18 -0700 Subject: [PATCH 065/234] Make Parse method generation optional. --- .../CommandGenerator.cs | 10 ++- .../ParserGenerator.cs | 82 ++++++++++++------- src/Ookii.CommandLine.Generator/TypeHelper.cs | 2 + .../Commands/ICommandProvider.cs | 30 +++++++ .../GeneratedParserAttribute.cs | 27 +++++- src/Samples/Parser/ProgramArguments.cs | 2 +- src/Samples/TrimTest/Program.cs | 8 -- 7 files changed, 118 insertions(+), 43 deletions(-) create mode 100644 src/Ookii.CommandLine/Commands/ICommandProvider.cs diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index afc55048..2fbe8a79 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -58,7 +58,13 @@ public void Generate() } var builder = new SourceBuilder(provider.ContainingNamespace); - builder.AppendLine($"partial class {provider.Name} : Ookii.CommandLine.Support.CommandProvider"); + builder.Append($"partial class {provider.Name} : Ookii.CommandLine.Support.CommandProvider"); + if (_typeHelper.ICommandProvider != null) + { + builder.Append(", Ookii.CommandLine.Commands.ICommandProvider"); + } + + builder.AppendLine(); builder.OpenBlock(); builder.AppendLine("public override Ookii.CommandLine.Support.ProviderKind Kind => Ookii.CommandLine.Support.ProviderKind.Generated;"); builder.AppendLine(); @@ -130,8 +136,6 @@ public void Generate() builder.AppendLine("yield break;"); builder.CloseBlock(); // GetCommandsUnsorted builder.AppendLine(); - - // TODO: Make optional. builder.AppendLine("public static Ookii.CommandLine.Commands.CommandManager CreateCommandManager(Ookii.CommandLine.Commands.CommandOptions? options = null)"); builder.AppendLine($" => new Ookii.CommandLine.Commands.CommandManager(new {provider.ToDisplayString()}(), options);"); builder.CloseBlock(); // class diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 00844017..6e2e9869 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -57,35 +57,6 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen } public string? Generate() - { - _builder.AppendLine($"partial class {_argumentsClass.Name}"); - if (_typeHelper.IParser != null) - { - _builder.AppendLine($" : Ookii.CommandLine.IParser<{_argumentsClass.Name}>"); - } - - _builder.OpenBlock(); - if (!GenerateProvider()) - { - return null; - } - - _builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{_argumentsClass.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new GeneratedProvider(), options);"); - _builder.AppendLine(); - var nullableType = _argumentsClass.WithNullableAnnotation(NullableAnnotation.Annotated); - // TODO: Optionally implement these. - // We cannot rely on default implementations, because that makes the methods uncallable - // without a generic type argument. - _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling();"); - _builder.AppendLine(); - _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); - _builder.AppendLine(); - _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, int index, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args, index);"); - _builder.CloseBlock(); // class - return _builder.GetSource(); - } - - private bool GenerateProvider() { // Find the attributes that can apply to an arguments class. // This code also finds attributes that inherit from those attribute. By instantiating the @@ -100,7 +71,7 @@ private bool GenerateProvider() if (_argumentsClass.ImplementsInterface(_typeHelper.ICommandWithCustomParsing)) { _context.ReportDiagnostic(Diagnostics.GeneratedCustomParsingCommand(_argumentsClass)); - return false; + return null; } else if (_argumentsClass.ImplementsInterface(_typeHelper.ICommand)) { @@ -116,6 +87,57 @@ private bool GenerateProvider() } } + // 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}"); + if (_typeHelper.IParser != null) + { + 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; + } + + _builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{_argumentsClass.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new GeneratedProvider(), 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($"public static {nullableType.ToDisplayString()} Parse(Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling();"); + _builder.AppendLine(); + _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); + _builder.AppendLine(); + _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, int index, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args, index);"); + _builder.CloseBlock(); // class + } + + return _builder.GetSource(); + } + + private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isCommand) + { _builder.AppendLine("private class GeneratedProvider : Ookii.CommandLine.Support.GeneratedArgumentProvider"); _builder.OpenBlock(); _builder.AppendLine("public GeneratedProvider()"); diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index 52108069..447dd201 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -85,4 +85,6 @@ public TypeHelper(Compilation compilation) public INamedTypeSymbol? ICommandWithCustomParsing => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommandWithCustomParsing"); + public INamedTypeSymbol? ICommandProvider => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommandProvider"); + } diff --git a/src/Ookii.CommandLine/Commands/ICommandProvider.cs b/src/Ookii.CommandLine/Commands/ICommandProvider.cs new file mode 100644 index 00000000..a41d9d91 --- /dev/null +++ b/src/Ookii.CommandLine/Commands/ICommandProvider.cs @@ -0,0 +1,30 @@ +#if NET7_0_OR_GREATER + +namespace Ookii.CommandLine.Commands; + +/// +/// Defines a mechanism for creating an instance of the class for a +/// command provider. +/// +/// +/// +/// This type is only available when using .Net 7 or later. +/// +/// +/// This interface is automatically implemented on a class (on .Net 7 and later only) when the +/// is used. +/// +/// +public interface ICommandProvider +{ + /// + /// Creates a command manager using the class that implements this interface as a provider. + /// + /// + /// The to use, or to use the default options. + /// + /// An instance of the class. + public abstract static CommandManager CreateCommandManager(CommandOptions? options = null); +} + +#endif diff --git a/src/Ookii.CommandLine/GeneratedParserAttribute.cs b/src/Ookii.CommandLine/GeneratedParserAttribute.cs index 2c21515d..c7c88b14 100644 --- a/src/Ookii.CommandLine/GeneratedParserAttribute.cs +++ b/src/Ookii.CommandLine/GeneratedParserAttribute.cs @@ -1,4 +1,5 @@ -using System; +using Ookii.CommandLine.Commands; +using System; namespace Ookii.CommandLine; @@ -9,4 +10,28 @@ namespace Ookii.CommandLine; [AttributeUsage(AttributeTargets.Class)] public sealed class GeneratedParserAttribute : Attribute { + /// + /// Gets or sets a value that indicates whether to generate static Parse methods for the + /// arguments class. + /// + /// + /// to generate static Parse methods; 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. + /// + /// + /// The default behavior is to generate the static Parse methods unless this property + /// is explicitly set to . However, if the class is a command (it + /// implements the interface and has the + /// attribute), the default is to not generate the static Parse methods + /// unless this property is explicitly set to . + /// + /// + public bool GenerateParseMethods { get; set; } = true; } diff --git a/src/Samples/Parser/ProgramArguments.cs b/src/Samples/Parser/ProgramArguments.cs index aed9d7a8..29f34105 100644 --- a/src/Samples/Parser/ProgramArguments.cs +++ b/src/Samples/Parser/ProgramArguments.cs @@ -145,7 +145,7 @@ class ProgramArguments // 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). diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index e67ff941..9d1ac261 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -17,14 +17,6 @@ // Console.WriteLine($"Hello, World! {arguments.Test}"); //} -class MyProvider : CommandProvider -{ - public override string? GetApplicationDescription() => "Trim Test"; - public override IEnumerable GetCommandsUnsorted(CommandManager manager) - { - yield return new GeneratedCommandInfo(manager, typeof(Arguments), new CommandAttribute(), new DescriptionAttribute("This is a command test"), createParser: options => Arguments.CreateParser(options)); - } -} [GeneratedCommandProvider] partial class TestProvider { } From ae5ece1f48bebc9dd74111403804dc94dec979b0 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 4 May 2023 17:15:11 -0700 Subject: [PATCH 066/234] Add source generation docs. --- docs/README.md | 1 + docs/SourceGeneration.md | 171 +++++++++++++++++++++++++++++++++++++++ docs/Subcommands.md | 4 +- 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 docs/SourceGeneration.md diff --git a/docs/README.md b/docs/README.md index f5b0e432..cf734cb0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ explanations of what they do and examples of their output. - [Generating usage help](UsageHelp.md) - [Argument validation and dependencies](Validation.md) - [Subcommands](Subcommands.md) +- [Source generation](SourceGeneration.md) - [LineWrappingTextWriter and other utilities](Utilities.md) - [Code snippets](CodeSnippets.md) - [Class library documentation](https://www.ookii.org/Link/CommandLineDoc) diff --git a/docs/SourceGeneration.md b/docs/SourceGeneration.md new file mode 100644 index 00000000..34c34a9d --- /dev/null +++ b/docs/SourceGeneration.md @@ -0,0 +1,171 @@ +# Source generation + +Ookii.CommandLine provides the option to use compile-time source generation to create a +`CommandLineParser` for an arguments type, and to create a `CommandManager`. Source generation +is only available for C# projects. + +## Generating a parser + +Normally, the `CommandLineParser` class uses runtime reflection to determine the command line +arguments defined by an arguments class. Instead, you can apply the `GeneratedParserAttribute` to +a class, which will use compile-time C# source generation to determine the arguments instead. This +approach has the following advantages: + +- Get [errors and warnings](TODO) at compile time for argument rule violations (such as a required + positional argument after an optional positional argument), ignored options (such as setting a + default value for a required attribute), and other problems (such as using the same position + number more than once, method arguments with the wrong signature, or using the + `CommandLineArgumentAttribute` on a private or read-only property). These would normally be + silently ignored or cause a runtime exception, but now you can catch problems during compilation. +- Allow your application to be + [trimmed](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options). The way + Ookii.CommandLine uses reflection prevents trimming entirely. +- Improved performance. Benchmarks show that instantiating a `CommandLineParser` using a + generated parser is up to thirty times faster than using reflection. However, since we're still + talking about microseconds, this is unlikely to matter much unless you're creating instances in a + loop for some reason. + +Generally, it's recommended to use source generation unless you have a reason not to. Source +generation puts the following constraints on application: + +- The arguments class must be in a project using C# 8 or later. +- The project must be compiled using a recent version of the .Net SDK (TODO: Exact version). You + can target older runtimes supported by Ookii.CommandLine, down to .Net Framework 4.6, but you + must build the project using an SDK that supports the source generator. +- If you use the `ArgumentConverterAttribute`, you must use the constructor that takes a `Type` + instance. The constructor that takes a string is not supported. + +Other than that, source generation offers all the same features that reflection does. + +To generate a parser, you must mark the class as `partial`, and use the `GeneratedParserAttribute` +on the class. + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineArgument] + public string? SomeArgument { get; set; } +} +``` + +### Using a generated parser + +When using the `GeneratedParserAttribute`, you must *not* use the regular `CommandLineParser` or +`CommandLineParser` constructor, or the static `CommandLineParser.Parse()` methods. These will +still use reflection, even if a generated parser is available for a class. + +> By default, these constructors and methods will throw an exception if you try to use them with a +> class that has the `GeneratedParserAttribute`, to prevent accidentally using reflection when it +> was not intended. If for some reason you need to use reflection on a class that has that +> attribute, you can set the `ParseOptions.AllowReflectionWithGeneratedParser` property to `true`. + +Instead, the source generator will add the following methods to the arguments class (where +`Arguments` is the name of your class): + +```csharp +public static CommandLineParser CreateParser(ParseOptions? options = null); + +public static Arguments? Parse(ParseOptions? options = null); + +public static Arguments? Parse(string[] args, ParseOptions? options = null); + +public static Arguments? Parse(string[] args, int index, ParseOptions? options = null); +``` + +If your project target .Net 7 or later, these methods will implement the `IParserProvider` +and `IParser` interfaces. + +Generating the `Parse()` methods is optional and can be disabled using the +`GeneratedParserAttribute.GenerateParseMethods` property. The `CreateParser()` method is always +generated. + +Use the `CreateParser()` method as an alternative to the `CommandLineParser` constructor, and the +`Parse()` methods as an alternative to the static `CommandLineParser.Parse()` methods. + +So, if you had the following code before using source generation: + +```csharp +var arguments = CommandLineParser.Parse(); +``` + +You would replace it with the following: + +```csharp +var arguments = Arguments.Parse(); +``` + +## Generating a command manager + +Just like the `CommandLineParser` class, the `CommandManager` class normally uses reflection to +locate all command classes in the assembly or assemblies you specify. Instead, you can create a +class with the `GeneratedCommandProviderAttribute` which can perform this same job at compile time. +This creates a command provider which can then be used to instantiate a `CommandManager` without +using reflection. + +Using a generated command provider has the same benefits and restrictions as a generated parser, +with one additional caveat: a generated command provider can only use commands defined in the same +assembly as the provider (TODO: update if changed). If you use commands from different or multiple +assemblies, you must continue to use the reflection version. + +To create a generated command provider, define a partial class with the +`GeneratedCommandProviderAttribute`: + +```csharp +[GeneratedCommandProvider] +partial class MyCommandProvider +{ +} +``` + +To use the generated provider, the source generator will add the following method to the class: + +```csharp +public static CommandManager CreateCommandManager(CommandOptions? options = null); +``` + +If your project targets .Net 7 or later, this method implements the `ICommandProvider` interface. + +If you had the following code before using source generation: + +```csharp +var manager = new CommandManager(); +``` + +You would replace it with the following: + +```csharp +var manager = MyCommandProvider.CreateCommandManager(); +``` + +### Commands with generated parsers + +You can apply the `GeneratedParserAttribute` to a command class, and the generated command provider +will use the generated parser rather than reflection for that command. + +```csharp +[Command] +[GeneratedParser] +partial class MyCommand : ICommand +{ + [CommandLineArgument] + public string? SomeArgument { get; set; } + + public int Run() + { + /* ... */ + } +} +``` + +Note that if you create a normal `CommandManager` instance which uses reflection, it will always use +reflection to create a parser for its commands, even if the command has the +`GeneratedParserAttribute`. + +The `GeneratedParserAttribute` works the same for command classes as it does for any other arguments +class, with one exception: the static `Parse()` methods are not generated by default for command +classes. You must explicitly set the `GeneratedParserAttribute.GenerateParseMethods` to `true` if +you want them generated. + +Next, we will take a look at several [utility classes](Utilities.md) provided, and used, by +Ookii.CommandLine. diff --git a/docs/Subcommands.md b/docs/Subcommands.md index c21c5c53..6ae31728 100644 --- a/docs/Subcommands.md +++ b/docs/Subcommands.md @@ -558,8 +558,8 @@ functionality. Providing native support for nested subcommands is planned for a future release. -The next page will take a look at several [utility classes](Utilities.md) provided, and used, by -Ookii.CommandLine. +The next page will discuss Ookii.CommandLine's [source generation](SourceGeneration.md) in more +detail. [`AliasAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_AliasAttribute.htm [`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm From 8539328c0f283784dede86a67421d7512a0791c1 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 4 May 2023 18:00:04 -0700 Subject: [PATCH 067/234] Simplified method for using source generation with CommandManager. --- docs/SourceGeneration.md | 41 +- .../CommandGenerator.cs | 38 +- .../ParserIncrementalGenerator.cs | 12 +- src/Ookii.CommandLine.Generator/TypeHelper.cs | 2 +- src/Ookii.CommandLine.Tests/CommandTypes.cs | 4 +- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 2 +- src/Ookii.CommandLine/CommandLineParser.cs | 6 +- .../CommandLineParserGeneric.cs | 4 +- src/Ookii.CommandLine/Commands/CommandInfo.cs | 9 +- .../Commands/CommandManager.cs | 1013 ++++++++--------- ...cs => GeneratedCommandManagerAttribute.cs} | 2 +- .../Commands/ICommandProvider.cs | 30 - .../Properties/Resources.Designer.cs | 2 +- .../Properties/Resources.resx | 2 +- .../Support/CommandProvider.cs | 2 +- .../ReflectionCommandInfo.cs | 7 +- .../ReflectionCommandProvider.cs | 6 +- src/Samples/TrimTest/Program.cs | 6 +- 18 files changed, 579 insertions(+), 609 deletions(-) rename src/Ookii.CommandLine/Commands/{GeneratedCommandProviderAttribute.cs => GeneratedCommandManagerAttribute.cs} (79%) delete mode 100644 src/Ookii.CommandLine/Commands/ICommandProvider.cs rename src/Ookii.CommandLine/{Commands => Support}/ReflectionCommandInfo.cs (91%) rename src/Ookii.CommandLine/{Commands => Support}/ReflectionCommandProvider.cs (88%) diff --git a/docs/SourceGeneration.md b/docs/SourceGeneration.md index 34c34a9d..f8b45460 100644 --- a/docs/SourceGeneration.md +++ b/docs/SourceGeneration.md @@ -34,6 +34,8 @@ generation puts the following constraints on application: must build the project using an SDK that supports the source generator. - If you use the `ArgumentConverterAttribute`, you must use the constructor that takes a `Type` instance. The constructor that takes a string is not supported. +- The arguments class may not be nested in another type. +- The arguments class may not be a generic type. Other than that, source generation offers all the same features that reflection does. @@ -99,49 +101,48 @@ var arguments = Arguments.Parse(); Just like the `CommandLineParser` class, the `CommandManager` class normally uses reflection to locate all command classes in the assembly or assemblies you specify. Instead, you can create a -class with the `GeneratedCommandProviderAttribute` which can perform this same job at compile time. -This creates a command provider which can then be used to instantiate a `CommandManager` without -using reflection. +class with the `GeneratedCommandManagerAttribute` which can perform this same job at compile time. -Using a generated command provider has the same benefits and restrictions as a generated parser, -with one additional caveat: a generated command provider can only use commands defined in the same -assembly as the provider (TODO: update if changed). If you use commands from different or multiple -assemblies, you must continue to use the reflection version. +Using a generated command manager has the same benefits and restrictions as a generated parser, +with one additional caveat: a generated command manager can only use commands defined in the same +assembly as the manager (TODO: update if changed). If you use commands from different or multiple +assemblies, you must continue to use the reflection method. -To create a generated command provider, define a partial class with the -`GeneratedCommandProviderAttribute`: +To create a generated command manager, define a partial class with the +`GeneratedCommandManagerAttribute`: ```csharp -[GeneratedCommandProvider] -partial class MyCommandProvider +[GeneratedCommandManager] +partial class MyCommandManager { } ``` -To use the generated provider, the source generator will add the following method to the class: +The source generator will make it so that the class derives from `CommandManager`, and add the +following constructor to the class: ```csharp -public static CommandManager CreateCommandManager(CommandOptions? options = null); +public MyCommandManager(CommandOptions? options = null) ``` -If your project targets .Net 7 or later, this method implements the `ICommandProvider` interface. - If you had the following code before using source generation: ```csharp var manager = new CommandManager(); +return manager.RunCommand() ?? 1; ``` -You would replace it with the following: +You would simply replace it with the following: ```csharp -var manager = MyCommandProvider.CreateCommandManager(); +var manager = new MyCommandManager(); +return manager.RunCommand() ?? 1; ``` ### Commands with generated parsers -You can apply the `GeneratedParserAttribute` to a command class, and the generated command provider -will use the generated parser rather than reflection for that command. +You can apply the `GeneratedParserAttribute` to a command class, and the generated command manager +will use the generated parser for that command. ```csharp [Command] @@ -165,7 +166,7 @@ reflection to create a parser for its commands, even if the command has the The `GeneratedParserAttribute` works the same for command classes as it does for any other arguments class, with one exception: the static `Parse()` methods are not generated by default for command classes. You must explicitly set the `GeneratedParserAttribute.GenerateParseMethods` to `true` if -you want them generated. +you want them to be generated. Next, we will take a look at several [utility classes](Utilities.md) provided, and used, by Ookii.CommandLine. diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index 2fbe8a79..29d6affe 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -9,8 +9,7 @@ internal class CommandGenerator private readonly TypeHelper _typeHelper; private readonly SourceProductionContext _context; private readonly List<(INamedTypeSymbol Type, ArgumentsClassAttributes? Attributes)> _commands = new(); - - private readonly List _providers = new(); + private readonly List _managers = new(); public CommandGenerator(TypeHelper typeHelper, SourceProductionContext context) { @@ -28,27 +27,27 @@ public void AddCommand(INamedTypeSymbol type) _commands.Add((type, null)); } - public void AddProvider(INamedTypeSymbol provider) + public void AddManager(INamedTypeSymbol provider) { - _providers.Add(provider); + _managers.Add(provider); } public void Generate() { - foreach (var provider in _providers) + foreach (var manager in _managers) { - var source = GenerateProvider(provider); + var source = GenerateManager(manager); if (source != null) { - _context.AddSource(provider.ToDisplayString().ToIdentifier(".g.cs"), SourceText.From(source, Encoding.UTF8)); + _context.AddSource(manager.ToDisplayString().ToIdentifier(".g.cs"), SourceText.From(source, Encoding.UTF8)); } } } - private string? GenerateProvider(INamedTypeSymbol provider) + private string? GenerateManager(INamedTypeSymbol manager) { AttributeData? descriptionAttribute = null; - foreach (var attribute in provider.ContainingAssembly.GetAttributes()) + foreach (var attribute in manager.ContainingAssembly.GetAttributes()) { if (attribute.AttributeClass?.DerivesFrom(_typeHelper.AssemblyDescriptionAttribute) ?? false) { @@ -57,14 +56,10 @@ public void Generate() } } - var builder = new SourceBuilder(provider.ContainingNamespace); - builder.Append($"partial class {provider.Name} : Ookii.CommandLine.Support.CommandProvider"); - if (_typeHelper.ICommandProvider != null) - { - builder.Append(", Ookii.CommandLine.Commands.ICommandProvider"); - } - - builder.AppendLine(); + var builder = new SourceBuilder(manager.ContainingNamespace); + builder.AppendLine($"partial class {manager.Name} : Ookii.CommandLine.Commands.CommandManager"); + builder.OpenBlock(); + builder.AppendLine("private class GeneratedProvider : Ookii.CommandLine.Support.CommandProvider"); builder.OpenBlock(); builder.AppendLine("public override Ookii.CommandLine.Support.ProviderKind Kind => Ookii.CommandLine.Support.ProviderKind.Generated;"); builder.AppendLine(); @@ -135,10 +130,13 @@ public void Generate() // Makes sure the function compiles if there are no commands. builder.AppendLine("yield break;"); builder.CloseBlock(); // GetCommandsUnsorted + builder.CloseBlock(); // provider class builder.AppendLine(); - builder.AppendLine("public static Ookii.CommandLine.Commands.CommandManager CreateCommandManager(Ookii.CommandLine.Commands.CommandOptions? options = null)"); - builder.AppendLine($" => new Ookii.CommandLine.Commands.CommandManager(new {provider.ToDisplayString()}(), options);"); - builder.CloseBlock(); // class + builder.AppendLine($"public {manager.Name}(Ookii.CommandLine.Commands.CommandOptions? options = null)"); + builder.AppendLine($" : base(new GeneratedProvider(), options)"); + builder.OpenBlock(); + builder.CloseBlock(); // ctor + builder.CloseBlock(); // manager class return builder.GetSource(); } } diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index 15e2b648..6f04c88e 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -14,7 +14,7 @@ public class ParserIncrementalGenerator : IIncrementalGenerator private enum ClassKind { Arguments, - CommandProvider, + CommandManager, Command, } @@ -96,9 +96,9 @@ private static void Execute(Compilation compilation, ImmutableArray continue; } - if (info.ClassKind == ClassKind.CommandProvider) + if (info.ClassKind == ClassKind.CommandManager) { - commandGenerator.AddProvider(symbol); + commandGenerator.AddManager(symbol); continue; } @@ -123,7 +123,7 @@ private static void Execute(Compilation compilation, ImmutableArray var classDeclaration = (ClassDeclarationSyntax)context.Node; var typeHelper = new TypeHelper(context.SemanticModel.Compilation); var generatedParserType = typeHelper.GeneratedParserAttribute; - var generatedCommandProviderType = typeHelper.GeneratedCommandProviderAttribute; + var GeneratedCommandManagerType = typeHelper.GeneratedCommandManagerAttribute; var commandType = typeHelper.CommandAttribute; var isCommand = false; foreach (var attributeList in classDeclaration.AttributeLists) @@ -142,9 +142,9 @@ private static void Execute(Compilation compilation, ImmutableArray return new(classDeclaration, ClassKind.Arguments); } - if (attributeType.SymbolEquals(generatedCommandProviderType)) + if (attributeType.SymbolEquals(GeneratedCommandManagerType)) { - return new(classDeclaration, ClassKind.CommandProvider); + return new(classDeclaration, ClassKind.CommandManager); } if (attributeType.SymbolEquals(commandType)) diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index 447dd201..47b1a626 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -79,7 +79,7 @@ public TypeHelper(Compilation compilation) public INamedTypeSymbol? CommandAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.CommandAttribute"); - public INamedTypeSymbol? GeneratedCommandProviderAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.GeneratedCommandProviderAttribute"); + public INamedTypeSymbol? GeneratedCommandManagerAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.GeneratedCommandManagerAttribute"); public INamedTypeSymbol? ICommand => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommand"); diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index 3ac3953e..50401bdf 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -5,8 +5,8 @@ namespace Ookii.CommandLine.Tests; -[GeneratedCommandProvider] -partial class GeneratedProvider { } +[GeneratedCommandManager] +partial class GeneratedManager { } [GeneratedParser] [Command("test")] diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 0800d886..101cc6c4 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -347,7 +347,7 @@ public static CommandManager CreateManager(ProviderKind kind, CommandOptions opt var manager = kind switch { ProviderKind.Reflection => new CommandManager(_commandAssembly, options), - ProviderKind.Generated => GeneratedProvider.CreateCommandManager(options), + ProviderKind.Generated => new GeneratedManager(options), _ => throw new InvalidOperationException() }; diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 4a5363e4..5628b9a2 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -290,7 +290,7 @@ private struct PrefixInfo /// The type indicated by has the /// attribute applied. Use the generated static CreateParser() or Parse() /// methods on the arguments type to access the generated parser. For subcommands, use a - /// command provider with the attribute to + /// command provider with the attribute to /// create a that will use generated parsers for subcommands. Set /// the property to /// to disable this exception. @@ -347,7 +347,7 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) /// property has the /// attribute applied. Use the generated static CreateParser() or Parse() /// methods on the arguments type to access the generated parser. For subcommands, use a - /// command provider with the attribute to + /// command provider with the attribute to /// create a that will use generated parsers for subcommands. Set /// the property to /// to disable this exception. @@ -1030,7 +1030,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// The type indicated by has the /// attribute applied. Use the generated static CreateParser() or Parse() /// methods on the arguments type to access the generated parser. For subcommands, use a - /// command provider with the attribute to + /// command provider with the attribute to /// create a that will use generated parsers for subcommands. Set /// the property to /// to disable this exception. diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index 6c0e818b..a7a88507 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -42,7 +42,7 @@ public class CommandLineParser : CommandLineParser /// The type indicated by has the /// attribute applied. Use the generated static CreateParser() or Parse() /// methods on the arguments type to access the generated parser. For subcommands, use a - /// command provider with the attribute to + /// command provider with the attribute to /// create a that will use generated parsers for subcommands. Set /// the property to /// to disable this exception. @@ -82,7 +82,7 @@ public CommandLineParser(ParseOptions? options = null) /// property has the /// attribute applied. Use the generated static CreateParser() or Parse() /// methods on the arguments type to access the generated parser. For subcommands, use a - /// command provider with the attribute to + /// command provider with the attribute to /// create a that will use generated parsers for subcommands. Set /// the property to /// to disable this exception. diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index 09c12eb0..007559de 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -1,4 +1,5 @@ -using System; +using Ookii.CommandLine.Support; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; @@ -271,7 +272,7 @@ public bool MatchesName(string name, IComparer? comparer = null) /// if was not a command. /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandProviderAttribute instead.")] + [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandManagerAttribute instead.")] #endif public static CommandInfo? TryCreate(Type commandType, CommandManager manager) => ReflectionCommandInfo.TryCreate(commandType, manager); @@ -294,7 +295,7 @@ public bool MatchesName(string name, IComparer? comparer = null) /// A class with information about the command. /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandProviderAttribute instead.")] + [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandManagerAttribute instead.")] #endif public static CommandInfo Create(Type commandType, CommandManager manager) => new ReflectionCommandInfo(commandType, null, manager); @@ -311,7 +312,7 @@ public static CommandInfo Create(Type commandType, CommandManager manager) /// is . /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandProviderAttribute instead.")] + [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandManagerAttribute instead.")] #endif public static bool IsCommand(Type commandType) => ReflectionCommandInfo.GetCommandAttribute(commandType) != null; diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index c2c5f071..9e290b92 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -8,579 +8,578 @@ 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. +/// +/// +/// 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. +/// +/// +/// Commands can be defined in a single assembly, or 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; + + /// + /// 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. + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] +#endif + public CommandManager(CommandOptions? options = null) + : this(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. + /// + protected CommandManager(CommandProvider provider, CommandOptions? options = null) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _options = options ?? new(); + } + /// - /// Provides functionality to find and instantiate subcommands. + /// 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. + /// + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] +#endif + public CommandManager(Assembly assembly, CommandOptions? options = null) + : this(new ReflectionCommandProvider(assembly ?? throw new ArgumentNullException(nameof(assembly))), 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 . + /// +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] +#endif + public CommandManager(IEnumerable assemblies, CommandOptions? options = null) + : this(new ReflectionCommandProvider(assemblies ?? throw new ArgumentNullException(nameof(assemblies))), options) + { + } + + /// + /// Gets the options used by this instance. + /// + /// + /// An instance of the class. + /// /// /// - /// 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. + /// 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. + /// + /// /// - /// 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. + /// 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 the kind of used to supply the commands. + /// + /// + /// One of the values of the enumeration. + /// + public ProviderKind ProviderKind => _provider.Kind; + + /// + /// Gets information about the commands. + /// + /// + /// Information about every subcommand defined in the assemblies, ordered by command name. + /// + /// /// - /// 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. + /// Commands that don't meet the criteria of the + /// predicate are not returned. /// /// - /// 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. + /// The automatic version command is added if the + /// property is and there is no command with a conflicting name. /// + /// + public IEnumerable GetCommands() + { + var commands = GetCommandsUnsortedAndFiltered(); + if (_options.AutoVersionCommand && + !commands.Any(c => _options.CommandNameComparer.Compare(c.Name, Properties.Resources.AutomaticVersionCommandName) == 0)) + { + 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 . + /// + /// /// - /// Commands can be defined in a single assembly, or multiple assemblies. + /// 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. /// - /// - /// 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 + public CommandInfo? GetCommand(string commandName) { - private readonly CommandProvider _provider; - 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. - /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandProviderAttribute instead.")] -#endif - public CommandManager(CommandOptions? options = null) - : this(Assembly.GetCallingAssembly(), options) + if (commandName == null) { + throw new ArgumentNullException(nameof(commandName)); } + var command = GetCommandsUnsortedAndFiltered() + .Where(c => c.MatchesName(commandName, Options.CommandNameComparer)) + .FirstOrDefault(); - /// - /// 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. - /// - public CommandManager(CommandProvider provider, CommandOptions? options = null) + if (command != null) { - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - _options = options ?? new(); + return command; } - /// - /// 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. - /// - /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandProviderAttribute instead.")] -#endif - public CommandManager(Assembly assembly, CommandOptions? options = null) - : this(new ReflectionCommandProvider(assembly ?? throw new ArgumentNullException(nameof(assembly))), options) + if (_options.AutoVersionCommand && + _options.CommandNameComparer.Compare(commandName, _options.AutoVersionCommandName()) == 0) { + return CommandInfo.GetAutomaticVersionCommand(this); } - /// - /// 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 . - /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandProviderAttribute instead.")] -#endif - public CommandManager(IEnumerable assemblies, CommandOptions? options = null) - : this(new ReflectionCommandProvider(assemblies ?? throw new ArgumentNullException(nameof(assemblies))), options) + 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) + { + if (args == null) { + throw new ArgumentNullException(nameof(args)); } - /// - /// 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 the kind of used to supply the commands. - /// - /// - /// One of the values of the enumeration. - /// - public ProviderKind ProviderKind => _provider.Kind; - - /// - /// 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() + if (index < 0 || index > args.Length) { - var commands = GetCommandsUnsortedAndFiltered(); - if (_options.AutoVersionCommand && - !commands.Any(c => _options.CommandNameComparer.Compare(c.Name, Properties.Resources.AutomaticVersionCommandName) == 0)) - { - var versionCommand = CommandInfo.GetAutomaticVersionCommand(this); - commands = commands.Append(versionCommand); - } - - return commands.OrderBy(c => c.Name, _options.CommandNameComparer); + throw new ArgumentOutOfRangeException(nameof(index)); } - /// - /// 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 command = GetCommandsUnsortedAndFiltered() - .Where(c => c.MatchesName(commandName, Options.CommandNameComparer)) - .FirstOrDefault(); - - if (command != null) - { - return command; - } - - if (_options.AutoVersionCommand && - _options.CommandNameComparer.Compare(commandName, _options.AutoVersionCommandName()) == 0) - { - return CommandInfo.GetAutomaticVersionCommand(this); - } + ParseResult = default; + var commandInfo = commandName == null + ? null + : GetCommand(commandName); + 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. - /// 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) + _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)); - } - - ParseResult = default; - var commandInfo = commandName == null - ? null - : GetCommand(commandName); - - if (commandInfo is not CommandInfo info) - { - WriteUsage(); - return null; - } - - _options.UsageWriter.CommandName = info.Name; - try - { - var (command, result) = info.CreateInstanceWithResult(args, index); - ParseResult = result; - return command; - } - finally - { - _options.UsageWriter.CommandName = null; - } + var (command, result) = info.CreateInstanceWithResult(args, index); + ParseResult = result; + return command; } - - /// - /// - /// 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) + finally { - 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); + _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() + /// + /// + /// 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) + { + if (args == null) { - // Skip the first argument, it's the application name. - return CreateCommand(Environment.GetCommandLineArgs(), 1); + throw new ArgumentNullException(nameof(args)); } - - /// - /// 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) + if (index < 0 || index > args.Length) { - var command = CreateCommand(commandName, args, index); - return command?.Run(); + throw new ArgumentOutOfRangeException(nameof(index)); } - /// - /// - /// 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) + string? commandName = null; + if (index < args.Length) { - var command = CreateCommand(args, index); - return command?.Run(); + commandName = args[index]; + ++index; } - /// - /// 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() - { - // Skip the first argument, it's the application name. - return RunCommand(Environment.GetCommandLineArgs(), 1); - } + return CreateCommand(commandName, args, index); + } - /// - /// - /// 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(); - } + /// + /// 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() + { + // Skip the first argument, it's the application name. + return CreateCommand(Environment.GetCommandLineArgs(), 1); + } - 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. - /// - /// - /// - /// 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(); - } + /// + /// 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) + { + var command = CreateCommand(commandName, args, index); + return command?.Run(); + } - 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. + /// + /// + /// + /// 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) + { + var command = CreateCommand(args, index); + 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. - /// - /// - public async Task RunCommandAsync() - { - var command = CreateCommand(); - 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. + /// 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() + { + // Skip the first argument, it's the application name. + return RunCommand(Environment.GetCommandLineArgs(), 1); + } - 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. + /// + /// + 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(); } - /// - /// 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 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) { - _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(); + } + + /// + /// + /// 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 _options.UsageWriter.GetCommandListUsage(this); + return await asyncCommand.RunAsync(); } - /// - /// 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() => _provider.GetApplicationDescription(); + return command?.Run(); + } - private IEnumerable GetCommandsUnsortedAndFiltered() - { - var commands = _provider.GetCommandsUnsorted(this); - if (_options.CommandFilter != null) - { - commands = commands.Where(c => _options.CommandFilter(c)); - } + /// + /// 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() + { + _options.UsageWriter.WriteCommandListUsage(this); + } - return commands; + /// + /// 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 _options.UsageWriter.GetCommandListUsage(this); + } + + /// + /// 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() => _provider.GetApplicationDescription(); + + private IEnumerable GetCommandsUnsortedAndFiltered() + { + var commands = _provider.GetCommandsUnsorted(this); + if (_options.CommandFilter != null) + { + commands = commands.Where(c => _options.CommandFilter(c)); } + + return commands; } } diff --git a/src/Ookii.CommandLine/Commands/GeneratedCommandProviderAttribute.cs b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs similarity index 79% rename from src/Ookii.CommandLine/Commands/GeneratedCommandProviderAttribute.cs rename to src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs index b59d13de..fc532dfe 100644 --- a/src/Ookii.CommandLine/Commands/GeneratedCommandProviderAttribute.cs +++ b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs @@ -10,6 +10,6 @@ namespace Ookii.CommandLine.Commands; /// TODO: Better docs. /// [AttributeUsage(AttributeTargets.Class)] -public sealed class GeneratedCommandProviderAttribute : Attribute +public sealed class GeneratedCommandManagerAttribute : Attribute { } diff --git a/src/Ookii.CommandLine/Commands/ICommandProvider.cs b/src/Ookii.CommandLine/Commands/ICommandProvider.cs deleted file mode 100644 index a41d9d91..00000000 --- a/src/Ookii.CommandLine/Commands/ICommandProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -#if NET7_0_OR_GREATER - -namespace Ookii.CommandLine.Commands; - -/// -/// Defines a mechanism for creating an instance of the class for a -/// command provider. -/// -/// -/// -/// This type is only available when using .Net 7 or later. -/// -/// -/// This interface is automatically implemented on a class (on .Net 7 and later only) when the -/// is used. -/// -/// -public interface ICommandProvider -{ - /// - /// Creates a command manager using the class that implements this interface as a provider. - /// - /// - /// The to use, or to use the default options. - /// - /// An instance of the class. - public abstract static CommandManager CreateCommandManager(CommandOptions? options = null); -} - -#endif diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 5ffd19c3..3fbffa80 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -520,7 +520,7 @@ internal static string PropertyIsReadOnlyFormat { } /// - /// Looks up a localized string similar to The arguments type {0} has the GeneratedParserAttribute applied, but reflection is being used to create a parser for it. Use the generated {0}.CreateParser() or {0}.Parse() methods to use the generated parser. For subcommands, use the GeneratedCommandProviderAttribute.. + /// Looks up a localized string similar to The arguments type {0} has the GeneratedParserAttribute applied, but reflection is being used to create a parser for it. Use the generated {0}.CreateParser() or {0}.Parse() methods to use the generated parser. For subcommands, use the GeneratedCommandManagerAttribute.. /// internal static string ReflectionWithGeneratedParserFormat { get { diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index a7384222..1f884881 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -400,6 +400,6 @@ The command does not use custom parsing. - The arguments type {0} has the GeneratedParserAttribute applied, but reflection is being used to create a parser for it. Use the generated {0}.CreateParser() or {0}.Parse() methods to use the generated parser. For subcommands, use the GeneratedCommandProviderAttribute. + The arguments type {0} has the GeneratedParserAttribute applied, but reflection is being used to create a parser for it. Use the generated {0}.CreateParser() or {0}.Parse() methods to use the generated parser. For subcommands, use the GeneratedCommandManagerAttribute. \ No newline at end of file diff --git a/src/Ookii.CommandLine/Support/CommandProvider.cs b/src/Ookii.CommandLine/Support/CommandProvider.cs index 3a745b89..f4a21e3d 100644 --- a/src/Ookii.CommandLine/Support/CommandProvider.cs +++ b/src/Ookii.CommandLine/Support/CommandProvider.cs @@ -7,7 +7,7 @@ namespace Ookii.CommandLine.Support; /// A source of commands for the . /// /// -/// This class is used by the source generator when using +/// This class is used by the source generator when using /// attribute. It should not normally be used by other code. /// public abstract class CommandProvider diff --git a/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs similarity index 91% rename from src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs rename to src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs index 0b1d9044..99dac15b 100644 --- a/src/Ookii.CommandLine/Commands/ReflectionCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs @@ -1,4 +1,5 @@ -using System; +using Ookii.CommandLine.Commands; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; @@ -9,11 +10,11 @@ using System.Text; using System.Threading.Tasks; -namespace Ookii.CommandLine.Commands; +namespace Ookii.CommandLine.Support; #if NET6_0_OR_GREATER -[RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandProviderAttribute instead.")] +[RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] #endif internal class ReflectionCommandInfo : CommandInfo { diff --git a/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs b/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs similarity index 88% rename from src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs rename to src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs index de98ecf1..9b744492 100644 --- a/src/Ookii.CommandLine/Commands/ReflectionCommandProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs @@ -1,4 +1,4 @@ -using Ookii.CommandLine.Support; +using Ookii.CommandLine.Commands; using System; using System.Collections.Generic; using System.Diagnostics; @@ -6,10 +6,10 @@ using System.Linq; using System.Reflection; -namespace Ookii.CommandLine.Commands; +namespace Ookii.CommandLine.Support; #if NET6_0_OR_GREATER -[RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandProviderAttribute instead.")] +[RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] #endif internal class ReflectionCommandProvider : CommandProvider { diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 9d1ac261..52447637 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -8,7 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -var manager = TestProvider.CreateCommandManager(); +var manager = new TestManager(); return manager.RunCommand() ?? 1; //var arguments = Arguments.Parse(); @@ -18,8 +18,8 @@ //} -[GeneratedCommandProvider] -partial class TestProvider { } +[GeneratedCommandManager] +partial class TestManager { } [GeneratedParser] [ParseOptions(CaseSensitive = true)] From 1d73bfaf12f94c64f6fde0b9d6695be37063afe4 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 12 May 2023 11:27:51 -0700 Subject: [PATCH 068/234] Only use public command classes from external assemblies. --- .../Commands.cs | 27 +++++++ .../Ookii.CommandLine.Tests.Commands.csproj | 17 +++++ .../ookii.snk | Bin 0 -> 596 bytes .../Ookii.CommandLine.Tests.csproj | 5 +- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 71 ++++++++++++++---- src/Ookii.CommandLine.Tests/ookii.public | Bin 160 -> 0 bytes src/Ookii.CommandLine.sln | 8 +- .../Commands/CommandManager.cs | 39 ++++++++-- .../Support/ReflectionCommandProvider.cs | 8 +- 9 files changed, 151 insertions(+), 24 deletions(-) create mode 100644 src/Ookii.CommandLine.Tests.Commands/Commands.cs create mode 100644 src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj create mode 100644 src/Ookii.CommandLine.Tests.Commands/ookii.snk delete mode 100644 src/Ookii.CommandLine.Tests/ookii.public diff --git a/src/Ookii.CommandLine.Tests.Commands/Commands.cs b/src/Ookii.CommandLine.Tests.Commands/Commands.cs new file mode 100644 index 00000000..2c4bfda5 --- /dev/null +++ b/src/Ookii.CommandLine.Tests.Commands/Commands.cs @@ -0,0 +1,27 @@ +// Commands to test loading commands from an external assembly. +using Ookii.CommandLine.Commands; + +namespace Ookii.CommandLine.Tests.Commands; + +[Command("external")] +public 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..54d43d92 --- /dev/null +++ b/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj @@ -0,0 +1,17 @@ + + + + net7.0;net6.0;net48 + enable + enable + 11.0 + true + ookii.snk + false + + + + + + + diff --git a/src/Ookii.CommandLine.Tests.Commands/ookii.snk b/src/Ookii.CommandLine.Tests.Commands/ookii.snk new file mode 100644 index 0000000000000000000000000000000000000000..1befa1340940be77d7ffda7dd5b28aec90a3a9e3 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097h;jblssCt@a0%K~(>Kgj%%NuoDbI&o} zu*b<8CIqceR|Pfm>E`4$C30P=)yO?!ABF=4 z-f_D#=T22nU5(%Yp}2`^x_>?Qe%nY`+?w_$mcIQ`YZ2u>?%*F-5wY;;ud`leSUaS< zN-yb7um7u$LUCIwSRxp+E7!>{kpRgZfuYSIv)NMhV1t6UHK-76f$?6f#(Wj{GZc0g zbLQ@qqCyz!2_vZad=byO2@z<>t93lsv(kYFB{N>U_Dm=NDR3=&yxw<7&f{R($=oDf zrvOl~R$+w0;}wBh=bXpujy(RZN@>j+A0e^0JEf9Ad=9zHe7%-@H}kSyW`>#Eanow_ z-#71bK+XbZo~d4NwiTB!Di!2 zRF3JjQXIPhRE?r5w|iM6l_&WD118pH<0ynJCn=HY+Cdj+?Du+N79N80+ryv%@PBlZ zq6SLd>_PcyH9|hee}>u2DqK?rYA9NA5wk@Rf*W41@1q`{9eo$>4{`LaN!m)DFnlJf iDg@evY}P48mDZW@#ET&MA_j(Cq3Xw#L3spw?UyBgz#jMj literal 0 HcmV?d00001 diff --git a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj index 4211ac00..3c3122fa 100644 --- a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj +++ b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj @@ -23,9 +23,8 @@ + - + diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 101cc6c4..3da68698 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -2,8 +2,10 @@ 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.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -29,18 +31,15 @@ public static void TestFixtureSetup(TestContext context) public void GetCommandsTest(ProviderKind kind) { var manager = CreateManager(kind); - 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); + 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("version", null) + ); } [TestMethod] @@ -330,6 +329,39 @@ public async Task TestAsyncCommandBase() Assert.AreEqual(42, actual); } + [TestMethod] + public void TestExplicitAssembly() + { + // Using the calling assembly explicitly loads all the commands, including internal, + // same as the default constructor. + var manager = new CommandManager(_commandAssembly); + Assert.AreEqual(6, manager.GetCommands().Count()); + + manager = new CommandManager(typeof(ExternalCommand).Assembly); + VerifyCommands( + manager.GetCommands(), + new("external", typeof(ExternalCommand)), + new("OtherExternalCommand", typeof(OtherExternalCommand)), + new("version", null) + ); + + manager = new CommandManager(new[] { typeof(ExternalCommand).Assembly, _commandAssembly }); + 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("version", null) + ); + } + + private record struct ExpectedCommand(string Name, Type Type, bool CustomParsing = false, params string[] Aliases); + + private static void VerifyCommand(CommandInfo command, string name, Type type, bool customParsing = false, string[] aliases = null) { Assert.AreEqual(name, command.Name); @@ -342,11 +374,24 @@ private static void VerifyCommand(CommandInfo command, string name, Type type, b CollectionAssert.AreEqual(aliases ?? Array.Empty(), command.Aliases.ToArray()); } + 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); + ++index; + } + } + + public static CommandManager CreateManager(ProviderKind kind, CommandOptions options = null) { var manager = kind switch { - ProviderKind.Reflection => new CommandManager(_commandAssembly, options), + ProviderKind.Reflection => new CommandManager(options), ProviderKind.Generated => new GeneratedManager(options), _ => throw new InvalidOperationException() }; diff --git a/src/Ookii.CommandLine.Tests/ookii.public b/src/Ookii.CommandLine.Tests/ookii.public deleted file mode 100644 index 67c4e827b5c53d2b87311f5e2faedeb50b010a9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160 zcmV;R0AK$ABme*efB*oL000060ssI2Bme+XQ$aBR1ONa50097h;jblssCt@a0%K~( z>Kgj%%NuoDbI&o}u*b<8CIqceR|Pfm>E`4$C30P=)y - /// Initializes a new instance of the class for the calling - /// assembly. + /// 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. /// + /// + /// + /// Both public and internal command classes will be used. + /// + /// + /// 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("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] #endif public CommandManager(CommandOptions? options = null) - : this(Assembly.GetCallingAssembly(), options) + : this(new ReflectionCommandProvider(Assembly.GetCallingAssembly(), Assembly.GetCallingAssembly()), options) { } @@ -99,6 +110,11 @@ protected CommandManager(CommandProvider provider, CommandOptions? options = nul /// is . /// /// + /// + /// If is the assembly that called this constructor, both public + /// and internal command classes will be used. Otherwise, only public command classes are + /// used. + /// /// /// Once a command is created, the instance may be modified /// with the options of the attribute applied to the @@ -110,7 +126,7 @@ protected CommandManager(CommandProvider provider, CommandOptions? options = nul [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] #endif public CommandManager(Assembly assembly, CommandOptions? options = null) - : this(new ReflectionCommandProvider(assembly ?? throw new ArgumentNullException(nameof(assembly))), options) + : this(new ReflectionCommandProvider(assembly ?? throw new ArgumentNullException(nameof(assembly)), Assembly.GetCallingAssembly()), options) { } @@ -125,11 +141,24 @@ public CommandManager(Assembly assembly, CommandOptions? options = null) /// /// or one of its elements is . /// + /// + /// + /// If an assembly in is the assembly that called this + /// constructor, both public and internal command classes will be used. Otherwise, only public + /// command classes are used for that assembly. + /// + /// + /// 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("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] #endif public CommandManager(IEnumerable assemblies, CommandOptions? options = null) - : this(new ReflectionCommandProvider(assemblies ?? throw new ArgumentNullException(nameof(assemblies))), options) + : this(new ReflectionCommandProvider(assemblies ?? throw new ArgumentNullException(nameof(assemblies)), Assembly.GetCallingAssembly()), options) { } diff --git a/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs b/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs index 9b744492..90a33239 100644 --- a/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs @@ -15,15 +15,18 @@ internal class ReflectionCommandProvider : CommandProvider { private readonly Assembly? _assembly; private readonly IEnumerable? _assemblies; + private readonly Assembly _callingAssembly; - public ReflectionCommandProvider(Assembly assembly) + public ReflectionCommandProvider(Assembly assembly, Assembly callingAssembly) { _assembly = assembly; + _callingAssembly = callingAssembly; } - public ReflectionCommandProvider(IEnumerable assemblies) + public ReflectionCommandProvider(IEnumerable assemblies, Assembly callingAssembly) { _assemblies = assemblies; + _callingAssembly = callingAssembly; if (_assemblies.Any(a => a == null)) { throw new ArgumentNullException(nameof(assemblies)); @@ -47,6 +50,7 @@ public override IEnumerable GetCommandsUnsorted(CommandManager mana } return from type in types + where type.Assembly == _callingAssembly || type.IsPublic let info = CommandInfo.TryCreate(type, manager) where info != null select info; From 89d54e7facff5b36cb3febf1c392ca50d8c590da Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 12 May 2023 13:20:01 -0700 Subject: [PATCH 069/234] Support using different command assemblies with source generator. --- .../ArgumentsClassAttributes.cs | 7 +- .../CommandGenerator.cs | 194 ++++++++++++++---- .../Diagnostics.cs | 16 ++ src/Ookii.CommandLine.Generator/Extensions.cs | 28 ++- .../Ookii.CommandLine.Generator.csproj | 3 + .../Properties/Resources.Designer.cs | 36 ++++ .../Properties/Resources.resx | 12 ++ src/Ookii.CommandLine.Generator/ookii.snk | Bin 0 -> 596 bytes .../Ookii.CommandLine.Tests.Commands.csproj | 4 +- src/Ookii.CommandLine.Tests/CommandTypes.cs | 8 + src/Ookii.CommandLine.Tests/SubCommandTest.cs | 27 ++- .../GeneratedCommandManagerAttribute.cs | 21 ++ 12 files changed, 306 insertions(+), 50 deletions(-) create mode 100644 src/Ookii.CommandLine.Generator/ookii.snk diff --git a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs index 382c946b..ebe87edf 100644 --- a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs +++ b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs @@ -12,7 +12,7 @@ internal readonly struct ArgumentsClassAttributes private readonly List? _classValidators; private readonly List? _aliases; - public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper, SourceProductionContext context) + public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper, SourceProductionContext? context) { // Exclude special types so we don't generate warnings for attributes on framework types. for (var current = symbol; current?.SpecialType == SpecialType.None; current = current.BaseType) @@ -30,7 +30,10 @@ public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper, Sourc continue; } - context.ReportDiagnostic(Diagnostics.IgnoredAttribute(attribute)); + if (context is SourceProductionContext c) + { + c.ReportDiagnostic(Diagnostics.IgnoredAttribute(attribute)); + } } } } diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index 29d6affe..58910770 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -1,11 +1,53 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; +using System.Diagnostics; using System.Text; namespace Ookii.CommandLine.Generator; internal class CommandGenerator { + #region Nested types + + private class CommandVisitor : SymbolVisitor + { + private readonly TypeHelper _typeHelper; + + public CommandVisitor(TypeHelper typeHelper) + { + _typeHelper = typeHelper; + } + + public List<(INamedTypeSymbol, ArgumentsClassAttributes?)> Commands { get; } = new(); + + public override void VisitAssembly(IAssemblySymbol symbol) + { + symbol.GlobalNamespace.Accept(this); + } + + public override void VisitNamespace(INamespaceSymbol symbol) + { + foreach (var member in symbol.GetMembers()) + { + member.Accept(this); + } + } + + public override void VisitNamedType(INamedTypeSymbol symbol) + { + if (symbol.DeclaredAccessibility == Accessibility.Public && symbol.ImplementsInterface(_typeHelper.ICommand)) + { + var attributes = new ArgumentsClassAttributes(symbol, _typeHelper, null); + if (attributes.Command != null) + { + Commands.Add((symbol, attributes)); + } + } + } + } + + #endregion + private readonly TypeHelper _typeHelper; private readonly SourceProductionContext _context; private readonly List<(INamedTypeSymbol Type, ArgumentsClassAttributes? Attributes)> _commands = new(); @@ -77,66 +119,140 @@ public void Generate() builder.AppendLine("public override System.Collections.Generic.IEnumerable GetCommandsUnsorted(Ookii.CommandLine.Commands.CommandManager manager)"); builder.OpenBlock(); - // TODO: Providers with custom command lists. - foreach (var command in _commands) + var generatedManagerAttribute = manager.GetAttribute(_typeHelper.GeneratedCommandManagerAttribute!)!; + if (generatedManagerAttribute.GetNamedArgument("AssemblyNames") is TypedConstant assemblies) { - var isGenerated = command.Attributes != null; - var useCustomParsing = command.Type.ImplementsInterface(_typeHelper.ICommandWithCustomParsing); - var commandTypeName = command.Type.ToDisplayString(); - if (useCustomParsing) + foreach (var assembly in assemblies.Values) { - builder.AppendLine($"yield return new Ookii.CommandLine.Support.GeneratedCommandInfoWithCustomParsing<{commandTypeName}>("); + var commands = GetCommands(assembly.Value as string, manager); + if (commands == null) + { + return null; + } + + foreach (var (command, attributes) in commands) + { + GenerateCommand(builder, command, attributes); + } } - else + } + else + { + foreach (var (command, attributes) in _commands) { - builder.AppendLine("yield return new Ookii.CommandLine.Support.GeneratedCommandInfo("); + GenerateCommand(builder, command, attributes); } + } + + // Makes sure the function compiles if there are no commands. + builder.AppendLine("yield break;"); + builder.CloseBlock(); // GetCommandsUnsorted + builder.CloseBlock(); // provider class + builder.AppendLine(); + builder.AppendLine($"public {manager.Name}(Ookii.CommandLine.Commands.CommandOptions? options = null)"); + builder.AppendLine($" : base(new GeneratedProvider(), options)"); + builder.OpenBlock(); + builder.CloseBlock(); // ctor + builder.CloseBlock(); // manager class + return builder.GetSource(); + } - builder.IncreaseIndent(); - builder.AppendLine("manager"); - if (!useCustomParsing) + private void GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType, ArgumentsClassAttributes? commandAttributes) + { + var useCustomParsing = commandType.ImplementsInterface(_typeHelper.ICommandWithCustomParsing); + var commandTypeName = commandType.ToDisplayString(); + if (useCustomParsing) + { + builder.AppendLine($"yield return new Ookii.CommandLine.Support.GeneratedCommandInfoWithCustomParsing<{commandTypeName}>("); + } + else + { + builder.AppendLine("yield return new Ookii.CommandLine.Support.GeneratedCommandInfo("); + } + + builder.IncreaseIndent(); + builder.AppendLine("manager"); + if (!useCustomParsing) + { + builder.AppendLine($", typeof({commandTypeName})"); + } + + var attributes = commandAttributes ?? new ArgumentsClassAttributes(commandType, _typeHelper, _context); + builder.AppendLine($", {attributes.Command!.CreateInstantiation()}"); + if (attributes.Description != null) + { + builder.AppendLine($", descriptionAttribute: {attributes.Description.CreateInstantiation()}"); + } + + if (attributes.Aliases != null) + { + builder.AppendLine($", aliasAttributes: new Ookii.CommandLine.AliasAttribute[] {{ {string.Join(", ", attributes.Aliases.Select(a => a.CreateInstantiation()))} }}"); + } + + if (!useCustomParsing) + { + if (attributes.GeneratedParser != null) + { + builder.AppendLine($", createParser: options => {commandTypeName}.CreateParser(options)"); + } + else { - builder.AppendLine($", typeof({commandTypeName})"); + builder.AppendLine($", createParser: options => new CommandLineParser<{commandTypeName}>(options)"); } + } - var attributes = command.Attributes ?? new ArgumentsClassAttributes(command.Type, _typeHelper, _context); - builder.AppendLine($", {attributes.Command!.CreateInstantiation()}"); - if (attributes.Description != null) + builder.DecreaseIndent(); + builder.AppendLine(");"); + } + + private IEnumerable<(INamedTypeSymbol, ArgumentsClassAttributes?)>? GetCommands(string? assemblyName, ITypeSymbol manager) + { + if (assemblyName == null) + { + _context.ReportDiagnostic(Diagnostics.InvalidAssemblyName(manager, "null")); + return null; + } + + AssemblyIdentity? identity = null; + if (assemblyName.Contains(",")) + { + if (!AssemblyIdentity.TryParseDisplayName(assemblyName, out identity)) { - builder.AppendLine($", descriptionAttribute: {attributes.Description.CreateInstantiation()}"); + _context.ReportDiagnostic(Diagnostics.InvalidAssemblyName(manager, assemblyName)); + return null; } - if (attributes.Aliases != null) + if (_typeHelper.Compilation.Assembly.Identity.Equals(identity)) { - builder.AppendLine($", aliasAttributes: new Ookii.CommandLine.AliasAttribute[] {{ {string.Join(", ", attributes.Aliases.Select(a => a.CreateInstantiation()))} }}"); + return _commands; } + } + else if (_typeHelper.Compilation.Assembly.Name == assemblyName) + { + return _commands; + } - if (!useCustomParsing) + IAssemblySymbol? foundAssembly = null; + foreach (var reference in _typeHelper.Compilation.References) + { + if (_typeHelper.Compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) { - if (isGenerated) - { - builder.AppendLine($", createParser: options => {commandTypeName}.CreateParser(options)"); - } - else + if (identity != null ? identity.Equals(assembly.Identity) : assembly.Name == assemblyName) { - builder.AppendLine($", createParser: options => new CommandLineParser<{commandTypeName}>(options)"); + foundAssembly = assembly; + break; } } + } - builder.DecreaseIndent(); - builder.AppendLine(");"); + if (foundAssembly == null) + { + _context.ReportDiagnostic(Diagnostics.UnknownAssemblyName(manager, assemblyName)); + return null; } - // Makes sure the function compiles if there are no commands. - builder.AppendLine("yield break;"); - builder.CloseBlock(); // GetCommandsUnsorted - builder.CloseBlock(); // provider class - builder.AppendLine(); - builder.AppendLine($"public {manager.Name}(Ookii.CommandLine.Commands.CommandOptions? options = null)"); - builder.AppendLine($" : base(new GeneratedProvider(), options)"); - builder.OpenBlock(); - builder.CloseBlock(); // ctor - builder.CloseBlock(); // manager class - return builder.GetSource(); + var visitor = new CommandVisitor(_typeHelper); + visitor.VisitAssembly(foundAssembly); + return visitor.Commands; } } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 9f34fcf4..5f105c45 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -113,6 +113,22 @@ public static Diagnostic PositionalRequiredArgumentAfterOptional(ISymbol symbol, symbol.ToDisplayString(), other); + public static Diagnostic InvalidAssemblyName(ISymbol symbol, string name) => CreateDiagnostic( + "CL0013", + nameof(Resources.InvalidAssemblyNameTitle), + nameof(Resources.InvalidAssemblyNameMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + name); + + public static Diagnostic UnknownAssemblyName(ISymbol symbol, string name) => CreateDiagnostic( + "CL0014", + nameof(Resources.UnknownAssemblyNameTitle), + nameof(Resources.UnknownAssemblyNameMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + name); + public static Diagnostic IgnoredAttribute(AttributeData attribute) => CreateDiagnostic( "CLW0001", nameof(Resources.UnknownAttributeTitle), diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index 2794e09b..ccbbd00b 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -174,7 +174,7 @@ public static bool CheckType(this AttributeData data, ITypeSymbol? attributeType } // Using a ref parameter with bool return allows me to chain these together. - public static bool CheckType(this AttributeData data, ITypeSymbol? attributeType, ref List? attributes) + public static bool CheckType(this AttributeData data, ITypeSymbol? attributeType, ref List? attributes) { if (!(data.AttributeClass?.DerivesFrom(attributeType) ?? false)) { @@ -185,4 +185,30 @@ public static bool CheckType(this AttributeData data, ITypeSymbol? attributeTyp attributes.Add(data); return true; } + + public static TypedConstant? GetNamedArgument(this AttributeData data, string name) + { + foreach (var arg in data.NamedArguments) + { + if (arg.Key == name) + { + return arg.Value; + } + } + + return null; + } + + public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol type) + { + foreach (var attribute in symbol.GetAttributes()) + { + if (type.SymbolEquals(attribute.AttributeClass)) + { + return attribute; + } + } + + return null; + } } diff --git a/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj b/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj index 7d934245..66d0c8b5 100644 --- a/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj +++ b/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj @@ -7,6 +7,9 @@ enable enable true + true + ookii.snk + false diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 9e628b6c..cf39c719 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -258,6 +258,24 @@ internal static string InvalidArrayRankTitle { } } + /// + /// 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.. /// @@ -474,6 +492,24 @@ internal static string ShortAliasWithoutShortNameTitle { } } + /// + /// 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 attribute {0} is unknown and will be ignored by the GeneratedParserAttribute.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 9b76a30b..b71e9b2a 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -183,6 +183,12 @@ 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. @@ -255,6 +261,12 @@ The ShortAliasAttribute is ignored on an argument with no short name. + + An assembly matching the name '{0}' was not found. + + + Unknown assembly name. + The attribute {0} is unknown and will be ignored by the GeneratedParserAttribute. diff --git a/src/Ookii.CommandLine.Generator/ookii.snk b/src/Ookii.CommandLine.Generator/ookii.snk new file mode 100644 index 0000000000000000000000000000000000000000..1befa1340940be77d7ffda7dd5b28aec90a3a9e3 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097h;jblssCt@a0%K~(>Kgj%%NuoDbI&o} zu*b<8CIqceR|Pfm>E`4$C30P=)yO?!ABF=4 z-f_D#=T22nU5(%Yp}2`^x_>?Qe%nY`+?w_$mcIQ`YZ2u>?%*F-5wY;;ud`leSUaS< zN-yb7um7u$LUCIwSRxp+E7!>{kpRgZfuYSIv)NMhV1t6UHK-76f$?6f#(Wj{GZc0g zbLQ@qqCyz!2_vZad=byO2@z<>t93lsv(kYFB{N>U_Dm=NDR3=&yxw<7&f{R($=oDf zrvOl~R$+w0;}wBh=bXpujy(RZN@>j+A0e^0JEf9Ad=9zHe7%-@H}kSyW`>#Eanow_ z-#71bK+XbZo~d4NwiTB!Di!2 zRF3JjQXIPhRE?r5w|iM6l_&WD118pH<0ynJCn=HY+Cdj+?Du+N79N80+ryv%@PBlZ zq6SLd>_PcyH9|hee}>u2DqK?rYA9NA5wk@Rf*W41@1q`{9eo$>4{`LaN!m)DFnlJf iDg@evY}P48mDZW@#ET&MA_j(Cq3Xw#L3spw?UyBgz#jMj literal 0 HcmV?d00001 diff --git a/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj b/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj index 54d43d92..0c137a6f 100644 --- a/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj +++ b/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj @@ -1,4 +1,4 @@ - + net7.0;net6.0;net48 @@ -8,6 +8,8 @@ true ookii.snk false + + 1.0.0 diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index 50401bdf..d3edcbee 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -8,6 +8,14 @@ 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.")] diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 3da68698..923a4802 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -330,14 +330,22 @@ public async Task TestAsyncCommandBase() } [TestMethod] - public void TestExplicitAssembly() + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestExplicitAssembly(ProviderKind kind) { - // Using the calling assembly explicitly loads all the commands, including internal, - // same as the default constructor. - var manager = new CommandManager(_commandAssembly); - Assert.AreEqual(6, manager.GetCommands().Count()); + 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(6, mgr.GetCommands().Count()); + } + + // Explicitly specify the external assembly, which loads only public commands. + var manager = kind == ProviderKind.Reflection + ? new CommandManager(typeof(ExternalCommand).Assembly) + : new GeneratedManagerWithExplicitAssembly(); - manager = new CommandManager(typeof(ExternalCommand).Assembly); VerifyCommands( manager.GetCommands(), new("external", typeof(ExternalCommand)), @@ -345,7 +353,12 @@ public void TestExplicitAssembly() new("version", null) ); - manager = new CommandManager(new[] { typeof(ExternalCommand).Assembly, _commandAssembly }); + // 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"), diff --git a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs index fc532dfe..d0690113 100644 --- a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs +++ b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Ookii.CommandLine.Commands; @@ -12,4 +13,24 @@ namespace Ookii.CommandLine.Commands; [AttributeUsage(AttributeTargets.Class)] 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 method; use the + /// constructor instead. + /// + /// + /// 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; } } From 3cc9c6abac30aa6550959a6b8f671d47a3f7080d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 12 May 2023 14:43:19 -0700 Subject: [PATCH 070/234] Generate default value descriptions in source generator. --- .../ConverterGenerator.cs | 2 - .../ParserGenerator.cs | 10 ++- src/Ookii.CommandLine/CommandLineArgument.cs | 72 ++++++++++++------- .../Support/GeneratedArgument.cs | 25 ++++++- 4 files changed, 75 insertions(+), 34 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index bbf0d745..beb5bf7c 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -42,7 +42,6 @@ public bool IsBetter(ConverterInfo other) #endregion - // TODO: Customizable or random namespace? private const string DefaultGeneratedNamespace = "Ookii.CommandLine.Conversion.Generated"; private const string ConverterSuffix = "Converter"; private readonly INamedTypeSymbol? _readOnlySpanType; @@ -180,7 +179,6 @@ private static string GenerateName(string displayName) private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, ConverterInfo info) { - // TODO: Handle exceptions similar to reflection versions. builder.AppendLine($"internal class {info.Name} : Ookii.CommandLine.Conversion.ArgumentConverter"); builder.OpenBlock(); string inputType = info.UseSpan ? "System.ReadOnlySpan" : "string"; diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 6e2e9869..346354ac 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -64,7 +64,6 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen // DescriptionAttribute that gets the description from a resource. var attributes = new ArgumentsClassAttributes(_argumentsClass, _typeHelper, _context); - // TODO: Warn if AliasAttribute without CommandAttribute. var isCommand = false; if (attributes.Command != null) { @@ -378,8 +377,6 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> return; } - // TODO: Default value description. Can make DetermineValueDescription abstract and move - // to ReflectionArgument when done. // 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(); @@ -392,14 +389,21 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.AppendLine($", attribute: {attributes.CommandLineArgument.CreateInstantiation()}"); _builder.AppendLine($", converter: {converter}"); _builder.AppendLine($", allowsNull: {(allowsNull.ToCSharpString())}"); + var valueDescriptionFormat = new SymbolDisplayFormat(genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters); if (keyType != null) { _builder.AppendLine($", keyType: typeof({keyType.ToDisplayString()})"); + _builder.AppendLine($", defaultKeyDescription: \"{keyType.ToDisplayString(valueDescriptionFormat)}\""); } if (valueType != null) { _builder.AppendLine($", valueType: typeof({valueType.ToDisplayString()})"); + _builder.AppendLine($", defaultValueDescription: \"{valueType.ToDisplayString(valueDescriptionFormat)}\""); + } + else + { + _builder.AppendLine($", defaultValueDescription: \"{elementType.ToDisplayString(valueDescriptionFormat)}\""); } AppendOptionalAttribute(attributes.MultiValueSeparator, "multiValueSeparatorAttribute"); diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 54790770..a069e347 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -597,8 +597,31 @@ public Type ArgumentType /// the type T; otherwise, the same value as the /// property. /// + /// + /// + /// For a dictionary argument, the element type is . + /// + /// public Type ElementType => _elementType; + /// + /// Gets the type of the keys of a dictionary argument. + /// + /// + /// The type of the keys in the dictionary, or if + /// is . + /// + public Type? KeyType => _keyType; + + /// + /// Gets the type of the values of a dictionary argument. + /// + /// + /// The type of the values in the dictionary, or if + /// is . + /// + public Type? ValueType => _valueType; + /// /// Gets the position of this argument. /// @@ -1178,35 +1201,11 @@ public override string ToString() /// Determines the value description if one wasn't explicitly given. /// /// - /// The type to get the description for, or null to use the value of the - /// property. + /// The type to get the description for. /// /// The value description. - /// - /// - /// This method is responsible for applying the , - /// if one is specified. - /// - /// - protected virtual string DetermineValueDescription(Type? type = null) - { - if (Kind == ArgumentKind.Dictionary && type == null) - { - var key = DetermineValueDescription(_keyType!.GetUnderlyingType()); - var value = DetermineValueDescription(_valueType!.GetUnderlyingType()); - return $"{key}{KeyValueSeparator}{value}"; - } - - var result = GetDefaultValueDescription(type); - if (result != null) - { - return result; - } - - var typeName = GetFriendlyTypeName(type ?? ElementType); - return Parser.Options.ValueDescriptionTransform?.Apply(typeName) ?? typeName; - } - + protected virtual string DetermineValueDescriptionForType(Type type) => GetFriendlyTypeName(type); + internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, Type argumentType, bool allowsNull, @@ -1252,6 +1251,25 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, }; } + private string DetermineValueDescription(Type? type = null) + { + var result = GetDefaultValueDescription(type); + if (result != null) + { + return result; + } + + if (Kind == ArgumentKind.Dictionary && type == null) + { + var key = DetermineValueDescription(_keyType!.GetUnderlyingType()); + var value = DetermineValueDescription(_valueType!.GetUnderlyingType()); + return $"{key}{KeyValueSeparator}{value}"; + } + + var typeName = DetermineValueDescriptionForType(type ?? ElementType); + return Parser.Options.ValueDescriptionTransform?.Apply(typeName) ?? typeName; + } + 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. diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index d29fc057..468b1674 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; @@ -18,13 +19,17 @@ 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) : base(info) + Func? callMethod, string defaultValueDescription, string? defaultKeyDescription) : base(info) { _setProperty = setProperty; _getProperty = getProperty; _callMethod = callMethod; + _defaultValueDescription = defaultValueDescription; + _defaultKeyDescription = defaultKeyDescription; } /// @@ -41,6 +46,8 @@ private GeneratedArgument(ArgumentInfo info, Action? setPropert /// /// /// + /// + /// /// /// /// @@ -63,6 +70,8 @@ public static GeneratedArgument Create(CommandLineParser parser, ArgumentKind kind, ArgumentConverter converter, bool allowsNull, + string defaultValueDescription, + string? defaultKeyDescription = null, bool requiredProperty = false, Type? keyType = null, Type? valueType = null, @@ -93,7 +102,7 @@ public static GeneratedArgument Create(CommandLineParser parser, info.ValueType = valueType; } - return new GeneratedArgument(info, setProperty, getProperty, callMethod); + return new GeneratedArgument(info, setProperty, getProperty, callMethod, defaultValueDescription, defaultKeyDescription); } /// @@ -131,4 +140,16 @@ protected override void SetProperty(object target, object? value) _setProperty(target, value); } + + /// + protected override string DetermineValueDescriptionForType(Type type) + { + Debug.Assert(type == KeyType || type == ValueType || (ValueType == null && type == ElementType)); + if (KeyType != null && type == KeyType) + { + return _defaultKeyDescription!; + } + + return _defaultValueDescription; + } } From 3d421baebc515bf7acf39c0b83d84df5476f24a0 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 12 May 2023 15:08:23 -0700 Subject: [PATCH 071/234] Diagnostic for unsupported ArgumentConverterAttribute constructor. --- .../Diagnostics.cs | 68 +++++++++++-------- .../ParserGenerator.cs | 16 ++--- .../ParserIncrementalGenerator.cs | 13 ++-- .../Properties/Resources.Designer.cs | 66 +++++++++++------- .../Properties/Resources.resx | 38 ++++++----- 5 files changed, 121 insertions(+), 80 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 5f105c45..de2586e3 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -6,37 +6,49 @@ namespace Ookii.CommandLine.Generator; -// TODO: Help URIs. internal static class Diagnostics { private const string Category = "Ookii.CommandLine"; - public static Diagnostic ArgumentsTypeNotReferenceType(INamedTypeSymbol symbol) => CreateDiagnostic( + public static Diagnostic TypeNotReferenceType(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( "CL0001", - nameof(Resources.ArgumentsTypeNotReferenceTypeTitle), - nameof(Resources.ArgumentsTypeNotReferenceTypeMessageFormat), + nameof(Resources.TypeNotReferenceTypeTitle), + nameof(Resources.TypeNotReferenceTypeMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(), + attributeName); - public static Diagnostic ArgumentsClassNotPartial(INamedTypeSymbol symbol) => CreateDiagnostic( + public static Diagnostic ClassNotPartial(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( "CL0002", - nameof(Resources.ArgumentsClassNotPartialTitle), - nameof(Resources.ArgumentsClassNotPartialMessageFormat), + nameof(Resources.ClassNotPartialTitle), + nameof(Resources.ClassNotPartialMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(), + attributeName); - public static Diagnostic ArgumentsClassIsGeneric(INamedTypeSymbol symbol) => CreateDiagnostic( + public static Diagnostic ClassIsGeneric(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( "CL0003", - nameof(Resources.ArgumentsClassIsGenericTitle), - nameof(Resources.ArgumentsClassIsGenericMessageFormat), + nameof(Resources.ClassIsGenericTitle), + nameof(Resources.ClassIsGenericMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(), + attributeName); - public static Diagnostic InvalidArrayRank(IPropertySymbol property) => CreateDiagnostic( + public static Diagnostic ClassIsNested(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( "CL0004", + nameof(Resources.ClassIsNestedTitle), + nameof(Resources.ClassIsNestedMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString(), + attributeName); + + + public static Diagnostic InvalidArrayRank(IPropertySymbol property) => CreateDiagnostic( + "CL0005", nameof(Resources.InvalidArrayRankTitle), nameof(Resources.InvalidArrayRankMessageFormat), DiagnosticSeverity.Error, @@ -45,15 +57,16 @@ public static Diagnostic InvalidArrayRank(IPropertySymbol property) => CreateDia property.Name); public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => CreateDiagnostic( - "CL0005", + "CL0006", nameof(Resources.PropertyIsReadOnlyTitle), nameof(Resources.PropertyIsReadOnlyMessageFormat), DiagnosticSeverity.Error, property.Locations.FirstOrDefault(), - property.ContainingType?.ToDisplayString(), property.Name); + property.ContainingType?.ToDisplayString(), + property.Name); public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => CreateDiagnostic( - "CL0006", + "CL0007", nameof(Resources.NoConverterTitle), nameof(Resources.NoConverterMessageFormat), DiagnosticSeverity.Error, @@ -63,7 +76,7 @@ public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => member.Name); public static Diagnostic InvalidMethodSignature(ISymbol method) => CreateDiagnostic( - "CL0007", + "CL0008", nameof(Resources.InvalidMethodSignatureTitle), nameof(Resources.InvalidMethodSignatureMessageFormat), DiagnosticSeverity.Error, @@ -71,21 +84,14 @@ public static Diagnostic InvalidMethodSignature(ISymbol method) => CreateDiagnos method.ContainingType?.ToDisplayString(), method.Name); - public static Diagnostic ArgumentsClassIsNested(INamedTypeSymbol symbol) => CreateDiagnostic( - "CL0008", - nameof(Resources.ArgumentsClassIsNestedTitle), - nameof(Resources.ArgumentsClassIsNestedMessageFormat), - DiagnosticSeverity.Error, - symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); - public static Diagnostic NonRequiredInitOnlyProperty(IPropertySymbol property) => CreateDiagnostic( "CL0009", nameof(Resources.NonRequiredInitOnlyPropertyTitle), nameof(Resources.NonRequiredInitOnlyPropertyMessageFormat), DiagnosticSeverity.Error, property.Locations.FirstOrDefault(), - property.ContainingType?.ToDisplayString(), property.Name); + property.ContainingType?.ToDisplayString(), + property.Name); public static Diagnostic GeneratedCustomParsingCommand(INamedTypeSymbol symbol) => CreateDiagnostic( "CL0010", @@ -129,6 +135,14 @@ public static Diagnostic UnknownAssemblyName(ISymbol symbol, string name) => Cre symbol.Locations.FirstOrDefault(), name); + public static Diagnostic ArgumentConverterStringNotSupported(ISymbol symbol) => CreateDiagnostic( + "CL0015", + nameof(Resources.ArgumentConverterStringNotSupportedTitle), + nameof(Resources.ArgumentConverterStringNotSupportedMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + public static Diagnostic IgnoredAttribute(AttributeData attribute) => CreateDiagnostic( "CLW0001", nameof(Resources.UnknownAttributeTitle), diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 346354ac..64289870 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -320,14 +320,14 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> valueType = rawValueType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); if (attributes.Converter == null) { - var keyConverter = DetermineConverter(keyType.GetUnderlyingType(), attributes.KeyConverter, keyType.IsNullableValueType()); + var keyConverter = DetermineConverter(member, keyType.GetUnderlyingType(), attributes.KeyConverter, keyType.IsNullableValueType()); if (keyConverter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); return; } - var valueConverter = DetermineConverter(valueType.GetUnderlyingType(), attributes.ValueConverter, valueType.IsNullableValueType()); + var valueConverter = DetermineConverter(member, valueType.GetUnderlyingType(), attributes.ValueConverter, valueType.IsNullableValueType()); if (valueConverter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); @@ -370,7 +370,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> } var elementType = namedElementTypeWithNullable?.GetUnderlyingType() ?? elementTypeWithNullable; - converter ??= DetermineConverter(elementType, attributes.Converter, elementTypeWithNullable.IsNullableValueType()); + converter ??= DetermineConverter(member, elementType, attributes.Converter, elementTypeWithNullable.IsNullableValueType()); if (converter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, elementType)); @@ -582,9 +582,9 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> return null; } - public string? DetermineConverter(ITypeSymbol elementType, AttributeData? converterAttribute, bool isNullableValueType) + public string? DetermineConverter(ISymbol member, ITypeSymbol elementType, AttributeData? converterAttribute, bool isNullableValueType) { - var converter = DetermineElementConverter(elementType, converterAttribute); + var converter = DetermineElementConverter(member, elementType, converterAttribute); if (converter != null && isNullableValueType) { converter = $"new Ookii.CommandLine.Conversion.NullableConverter({converter})"; @@ -593,15 +593,15 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> return converter; } - public string? DetermineElementConverter(ITypeSymbol elementType, AttributeData? converterAttribute) + public string? DetermineElementConverter(ISymbol member, ITypeSymbol elementType, AttributeData? converterAttribute) { if (converterAttribute != null) { var argument = converterAttribute.ConstructorArguments[0]; if (argument.Kind != TypedConstantKind.Type) { - // TODO: Either support this or emit error. - throw new NotSupportedException(); + _context.ReportDiagnostic(Diagnostics.ArgumentConverterStringNotSupported(member)); + return null; } var converterType = (INamedTypeSymbol)argument.Value!; diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index 6f04c88e..e15b72a2 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -71,28 +71,31 @@ private static void Execute(Compilation compilation, ImmutableArray continue; } - // TODO: Custom messages for provider types. + var attributeName = info.ClassKind == ClassKind.CommandManager + ? typeHelper.GeneratedCommandManagerAttribute!.Name + : typeHelper.GeneratedParserAttribute!.Name; + if (!symbol.IsReferenceType) { - context.ReportDiagnostic(Diagnostics.ArgumentsTypeNotReferenceType(symbol)); + context.ReportDiagnostic(Diagnostics.TypeNotReferenceType(symbol, attributeName)); continue; } if (!syntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) { - context.ReportDiagnostic(Diagnostics.ArgumentsClassNotPartial(symbol)); + context.ReportDiagnostic(Diagnostics.ClassNotPartial(symbol, attributeName)); continue; } if (symbol.IsGenericType) { - context.ReportDiagnostic(Diagnostics.ArgumentsClassIsGeneric(symbol)); + context.ReportDiagnostic(Diagnostics.ClassIsGeneric(symbol, attributeName)); continue; } if (symbol.ContainingType != null) { - context.ReportDiagnostic(Diagnostics.ArgumentsClassIsNested(symbol)); + context.ReportDiagnostic(Diagnostics.ClassIsNested(symbol, attributeName)); continue; } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index cf39c719..648604d9 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -79,74 +79,74 @@ internal static string AliasWithoutLongNameTitle { } /// - /// Looks up a localized string similar to The command line arguments class {0} may not be a generic class when the GeneratedParserAttribute is used.. + /// Looks up a localized string similar to The command line argument defined by {0} uses the ArgumentConverterAttribute with a string argument, which is not supported by the GeneratedParserAttribute. Use a Type argument instead by using the typeof keyword.. /// - internal static string ArgumentsClassIsGenericMessageFormat { + internal static string ArgumentConverterStringNotSupportedMessageFormat { get { - return ResourceManager.GetString("ArgumentsClassIsGenericMessageFormat", resourceCulture); + return ResourceManager.GetString("ArgumentConverterStringNotSupportedMessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to The command line arguments class may not be a generic type.. + /// Looks up a localized string similar to The ArgumentConverterAttribute must use the typeof keyword.. /// - internal static string ArgumentsClassIsGenericTitle { + internal static string ArgumentConverterStringNotSupportedTitle { get { - return ResourceManager.GetString("ArgumentsClassIsGenericTitle", resourceCulture); + return ResourceManager.GetString("ArgumentConverterStringNotSupportedTitle", resourceCulture); } } /// - /// Looks up a localized string similar to The command line arguments class {0} may not be nested in another type when the GeneratedParserAttribute is used.. + /// 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 ArgumentsClassIsNestedMessageFormat { + internal static string ClassIsGenericMessageFormat { get { - return ResourceManager.GetString("ArgumentsClassIsNestedMessageFormat", resourceCulture); + return ResourceManager.GetString("ClassIsGenericMessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to The command line arguments class may not be a nested type.. + /// Looks up a localized string similar to Th eclass may not be a generic type.. /// - internal static string ArgumentsClassIsNestedTitle { + internal static string ClassIsGenericTitle { get { - return ResourceManager.GetString("ArgumentsClassIsNestedTitle", resourceCulture); + return ResourceManager.GetString("ClassIsGenericTitle", resourceCulture); } } /// - /// Looks up a localized string similar to The command line arguments class {0} must use the 'partial' modifier.. + /// 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 ArgumentsClassNotPartialMessageFormat { + internal static string ClassIsNestedMessageFormat { get { - return ResourceManager.GetString("ArgumentsClassNotPartialMessageFormat", resourceCulture); + return ResourceManager.GetString("ClassIsNestedMessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to The command line arguments class must be a partial class.. + /// Looks up a localized string similar to The class may not be a nested type.. /// - internal static string ArgumentsClassNotPartialTitle { + internal static string ClassIsNestedTitle { get { - return ResourceManager.GetString("ArgumentsClassNotPartialTitle", resourceCulture); + return ResourceManager.GetString("ClassIsNestedTitle", resourceCulture); } } /// - /// Looks up a localized string similar to The command line arguments type {0} must be a reference type (class).. + /// Looks up a localized string similar to The class {0} must use the 'partial' modifier when the {1} attribute is used.. /// - internal static string ArgumentsTypeNotReferenceTypeMessageFormat { + internal static string ClassNotPartialMessageFormat { get { - return ResourceManager.GetString("ArgumentsTypeNotReferenceTypeMessageFormat", resourceCulture); + return ResourceManager.GetString("ClassNotPartialMessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to The command line arguments type must be a reference type.. + /// Looks up a localized string similar to The class must be a partial class.. /// - internal static string ArgumentsTypeNotReferenceTypeTitle { + internal static string ClassNotPartialTitle { get { - return ResourceManager.GetString("ArgumentsTypeNotReferenceTypeTitle", resourceCulture); + return ResourceManager.GetString("ClassNotPartialTitle", resourceCulture); } } @@ -492,6 +492,24 @@ internal static string ShortAliasWithoutShortNameTitle { } } + /// + /// 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 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.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index b71e9b2a..d9a34b54 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -123,29 +123,29 @@ The AliasAttribute is ignored on an argument with no long name. - - The command line arguments class {0} may not be a generic class when the GeneratedParserAttribute is used. + + The command line argument defined by {0} uses the ArgumentConverterAttribute with a string argument, which is not supported by the GeneratedParserAttribute. Use a Type argument instead by using the typeof keyword. - - The command line arguments class may not be a generic type. + + The ArgumentConverterAttribute must use the typeof keyword. - - The command line arguments class {0} may not be nested in another type when the GeneratedParserAttribute is used. + + The class {0} may not be a generic class when the {1} attribute is used. - - The command line arguments class may not be a nested type. + + Th eclass may not be a generic type. - - The command line arguments class {0} must use the 'partial' modifier. + + The class {0} may not be nested in another type when the {1} attribute is used. - - The command line arguments class must be a partial class. + + The class may not be a nested type. - - The command line arguments type {0} must be a reference type (class). + + The class {0} must use the 'partial' modifier when the {1} attribute is used. - - The command line arguments type must be a reference type. + + The class must be a partial class. The command line arguments class {0} has the CommandAttribute but does not implement the ICommand interface. @@ -261,6 +261,12 @@ 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 type must be a reference type. + An assembly matching the name '{0}' was not found. From 5186a1b2f32db361d1a919231b5f4ede94f50dd7 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 12 May 2023 15:29:55 -0700 Subject: [PATCH 072/234] Diagnostics for dictionary and multi-value specific attributes. --- .../Diagnostics.cs | 27 ++++++++ .../ParserGenerator.cs | 27 ++++++++ .../Properties/Resources.Designer.cs | 65 ++++++++++++++++++- .../Properties/Resources.resx | 23 ++++++- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 2 +- 5 files changed, 141 insertions(+), 3 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index de2586e3..2f2b46a8 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -250,6 +250,33 @@ public static Diagnostic InvalidGeneratedConverterNamespace(string ns, Attribute attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), ns); + public static Diagnostic IgnoredAttributeForNonDictionary(ISymbol member, AttributeData attribute) => CreateDiagnostic( + "CLW0011", + nameof(Resources.IgnoredAttributeForNonDictionaryTitle), + nameof(Resources.IgnoredAttributeForNonDictionaryMessageFormat), + DiagnosticSeverity.Warning, + attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), + attribute.AttributeClass?.Name, + member.ToDisplayString()); + + public static Diagnostic IgnoredAttributeForDictionaryWithConverter(ISymbol member, AttributeData attribute) => CreateDiagnostic( + "CLW0012", + nameof(Resources.IgnoredAttributeForDictionaryWithConverterTitle), + nameof(Resources.IgnoredAttributeForDictionaryWithConverterMessageFormat), + DiagnosticSeverity.Warning, + attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), + attribute.AttributeClass?.Name, + member.ToDisplayString()); + + public static Diagnostic IgnoredAttributeForNonMultiValue(ISymbol member, AttributeData attribute) => CreateDiagnostic( + "CLW0013", + nameof(Resources.IgnoredAttributeForNonMultiValueTitle), + nameof(Resources.IgnoredAttributeForNonMultiValueMessageFormat), + DiagnosticSeverity.Warning, + attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), + attribute.AttributeClass?.Name, + member.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 64289870..f78e7ab2 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -295,6 +295,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> } var isMultiValue = false; + var isDictionary = false; var isRequired = argumentInfo.IsRequired; var kind = "Ookii.CommandLine.ArgumentKind.SingleValue"; string? converter = null; @@ -311,6 +312,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> Debug.Assert(multiValueElementType != null); kind = "Ookii.CommandLine.ArgumentKind.Dictionary"; isMultiValue = true; + isDictionary = true; elementTypeWithNullable = multiValueElementType!; // KeyValuePair is guaranteed a named type. namedElementTypeWithNullable = (INamedTypeSymbol)elementTypeWithNullable; @@ -521,6 +523,14 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> { _context.ReportDiagnostic(Diagnostics.IsHiddenWithPositional(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)); + } } private (ITypeSymbol?, INamedTypeSymbol?, ITypeSymbol?)? DetermineMultiValueType(IPropertySymbol property, ITypeSymbol argumentType) @@ -740,4 +750,21 @@ private bool VerifyPositionalArgumentRules() 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)); + } + } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 648604d9..5331829c 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -106,7 +106,7 @@ internal static string ClassIsGenericMessageFormat { } /// - /// Looks up a localized string similar to Th eclass may not be a generic type.. + /// Looks up a localized string similar to The class may not be a generic type.. /// internal static string ClassIsGenericTitle { get { @@ -240,6 +240,60 @@ internal static string GeneratedCustomParsingCommandTitle { } } + /// + /// 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 {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-dictionary argument.. + /// + internal static string IgnoredAttributeForNonMultiValueTitle { + get { + return ResourceManager.GetString("IgnoredAttributeForNonMultiValueTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to The multi-value command line argument defined by {0}.{1} must have an array rank of one.. /// @@ -492,6 +546,15 @@ internal static string ShortAliasWithoutShortNameTitle { } } + /// + /// Looks up a localized string similar to . + /// + internal static string String1 { + get { + return ResourceManager.GetString("String1", resourceCulture); + } + } + /// /// Looks up a localized string similar to The type {0} must be a reference type (class) when the {1} attribute is used.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index d9a34b54..a2dfedd3 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -133,7 +133,7 @@ The class {0} may not be a generic class when the {1} attribute is used. - Th eclass may not be a generic type. + The class may not be a generic type. The class {0} may not be nested in another type when the {1} attribute is used. @@ -177,6 +177,24 @@ 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 {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-dictionary argument. + The multi-value command line argument defined by {0}.{1} must have an array rank of one. @@ -261,6 +279,9 @@ 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. diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 2ab6461e..4c6b8566 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -9,7 +9,7 @@ using System.Net; // We deliberately have some properties and methods that cause warnings, so disable those. -#pragma warning disable CLW0002,CLW0003,CLW0005,CLW0008,CLW0009 +#pragma warning disable CLW0002,CLW0003,CLW0005,CLW0008,CLW0009,CLW0013 namespace Ookii.CommandLine.Tests; From 628c85c926b322ed5ea5fd8cf8d78aabf17d7880 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 12 May 2023 15:31:14 -0700 Subject: [PATCH 073/234] Diagnostic for ignored AllowDuplicateDictionaryKeysAttribute. --- src/Ookii.CommandLine.Generator/ParserGenerator.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index f78e7ab2..f32d2371 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -531,6 +531,11 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> { _context.ReportDiagnostic(Diagnostics.IgnoredAttributeForNonMultiValue(member, attributes.MultiValueSeparator)); } + + if (!isDictionary && attributes.AllowDuplicateDictionaryKeys != null) + { + _context.ReportDiagnostic(Diagnostics.IgnoredAttributeForNonDictionary(member, attributes.AllowDuplicateDictionaryKeys)); + } } private (ITypeSymbol?, INamedTypeSymbol?, ITypeSymbol?)? DetermineMultiValueType(IPropertySymbol property, ITypeSymbol argumentType) From 37f62f34392e7a8a457b09ce2f9417ed0f229a88 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 12 May 2023 15:39:59 -0700 Subject: [PATCH 074/234] Improved diagnostic locations for attributes. --- .../Diagnostics.cs | 22 +++++++++---------- src/Ookii.CommandLine.Generator/Extensions.cs | 3 +++ .../ParserGenerator.cs | 6 ++--- src/Samples/TrimTest/Program.cs | 1 + 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 2f2b46a8..19460244 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -135,12 +135,12 @@ public static Diagnostic UnknownAssemblyName(ISymbol symbol, string name) => Cre symbol.Locations.FirstOrDefault(), name); - public static Diagnostic ArgumentConverterStringNotSupported(ISymbol symbol) => CreateDiagnostic( + public static Diagnostic ArgumentConverterStringNotSupported(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( "CL0015", nameof(Resources.ArgumentConverterStringNotSupportedTitle), nameof(Resources.ArgumentConverterStringNotSupportedMessageFormat), DiagnosticSeverity.Error, - symbol.Locations.FirstOrDefault(), + attribute.GetLocation(), symbol.ToDisplayString()); public static Diagnostic IgnoredAttribute(AttributeData attribute) => CreateDiagnostic( @@ -148,7 +148,7 @@ public static Diagnostic IgnoredAttribute(AttributeData attribute) => CreateDiag nameof(Resources.UnknownAttributeTitle), nameof(Resources.UnknownAttributeMessageFormat), DiagnosticSeverity.Warning, - attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), + attribute.GetLocation(), attribute.AttributeClass?.Name); public static Diagnostic NonPublicStaticMethod(ISymbol method) => CreateDiagnostic( @@ -218,20 +218,20 @@ public static Diagnostic DuplicatePosition(ISymbol symbol, string otherName) => symbol.ToDisplayString(), otherName); - public static Diagnostic ShortAliasWithoutShortName(ISymbol symbol) => CreateDiagnostic( + public static Diagnostic ShortAliasWithoutShortName(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( "CLW0008", nameof(Resources.ShortAliasWithoutShortNameTitle), nameof(Resources.ShortAliasWithoutShortNameMessageFormat), DiagnosticSeverity.Warning, - symbol.Locations.FirstOrDefault(), + attribute.GetLocation(), symbol.ToDisplayString()); - public static Diagnostic AliasWithoutLongName(ISymbol symbol) => CreateDiagnostic( + public static Diagnostic AliasWithoutLongName(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( "CLW0009", nameof(Resources.AliasWithoutLongNameTitle), nameof(Resources.AliasWithoutLongNameMessageFormat), DiagnosticSeverity.Warning, - symbol.Locations.FirstOrDefault(), + attribute.GetLocation(), symbol.ToDisplayString()); public static Diagnostic IsHiddenWithPositional(ISymbol symbol) => CreateDiagnostic( @@ -247,7 +247,7 @@ public static Diagnostic InvalidGeneratedConverterNamespace(string ns, Attribute nameof(Resources.InvalidGeneratedConverterNamespaceTitle), nameof(Resources.InvalidGeneratedConverterNamespaceMessageFormat), DiagnosticSeverity.Warning, - attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), + attribute.GetLocation(), ns); public static Diagnostic IgnoredAttributeForNonDictionary(ISymbol member, AttributeData attribute) => CreateDiagnostic( @@ -255,7 +255,7 @@ public static Diagnostic IgnoredAttributeForNonDictionary(ISymbol member, Attrib nameof(Resources.IgnoredAttributeForNonDictionaryTitle), nameof(Resources.IgnoredAttributeForNonDictionaryMessageFormat), DiagnosticSeverity.Warning, - attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), + attribute.GetLocation(), attribute.AttributeClass?.Name, member.ToDisplayString()); @@ -264,7 +264,7 @@ public static Diagnostic IgnoredAttributeForDictionaryWithConverter(ISymbol memb nameof(Resources.IgnoredAttributeForDictionaryWithConverterTitle), nameof(Resources.IgnoredAttributeForDictionaryWithConverterMessageFormat), DiagnosticSeverity.Warning, - attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), + attribute.GetLocation(), attribute.AttributeClass?.Name, member.ToDisplayString()); @@ -273,7 +273,7 @@ public static Diagnostic IgnoredAttributeForNonMultiValue(ISymbol member, Attrib nameof(Resources.IgnoredAttributeForNonMultiValueTitle), nameof(Resources.IgnoredAttributeForNonMultiValueMessageFormat), DiagnosticSeverity.Warning, - attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span), + attribute.GetLocation(), attribute.AttributeClass?.Name, member.ToDisplayString()); diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index ccbbd00b..a690f4f0 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -211,4 +211,7 @@ public static bool CheckType(this AttributeData data, ITypeSymbol? attributeType return null; } + + public static Location? GetLocation(this AttributeData attribute) + => attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span); } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index f32d2371..8ee526fd 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -511,12 +511,12 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> if (!argumentInfo.IsShort && attributes.ShortAliases != null) { - _context.ReportDiagnostic(Diagnostics.ShortAliasWithoutShortName(member)); + _context.ReportDiagnostic(Diagnostics.ShortAliasWithoutShortName(attributes.ShortAliases.First(), member)); } if (!argumentInfo.IsLong && attributes.Aliases != null) { - _context.ReportDiagnostic(Diagnostics.AliasWithoutLongName(member)); + _context.ReportDiagnostic(Diagnostics.AliasWithoutLongName(attributes.Aliases.First(), member)); } if (argumentInfo.IsHidden && argumentInfo.Position != null) @@ -615,7 +615,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> var argument = converterAttribute.ConstructorArguments[0]; if (argument.Kind != TypedConstantKind.Type) { - _context.ReportDiagnostic(Diagnostics.ArgumentConverterStringNotSupported(member)); + _context.ReportDiagnostic(Diagnostics.ArgumentConverterStringNotSupported(converterAttribute, member)); return null; } diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 52447637..e073fb09 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -39,6 +39,7 @@ partial class Arguments : ICommand [ValueDescription("Stuff")] [KeyValueSeparator("==")] [MultiValueSeparator] + [ShortAlias('c')] public Dictionary Test2 { get; set; } = default!; [CommandLineArgument] From 02d571b09c3040d4b0c904327f17b5ecb46b8ee4 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 12 May 2023 15:44:30 -0700 Subject: [PATCH 075/234] Undo test change. --- src/Samples/TrimTest/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index e073fb09..52447637 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -39,7 +39,6 @@ partial class Arguments : ICommand [ValueDescription("Stuff")] [KeyValueSeparator("==")] [MultiValueSeparator] - [ShortAlias('c')] public Dictionary Test2 { get; set; } = default!; [CommandLineArgument] From b2d9890944650ec260abcba3fffa841710749577 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 15 May 2023 17:00:14 -0700 Subject: [PATCH 076/234] Get default value from property initializers so they can be used for usage help. --- .../ParserGenerator.cs | 18 ++++++++++++++++++ src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 13 +++++++++++++ .../CommandLineParserTest.cs | 10 ++++++++++ .../Support/GeneratedArgument.cs | 3 +++ src/Samples/TrimTest/Program.cs | 4 ++-- 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 8ee526fd..546945ba 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; using System; using System.Data; @@ -449,6 +450,16 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> { _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) + { + var alternateDefaultValue = GetInitializerValue(property); + if (alternateDefaultValue != null) + { + _builder.AppendLine($", alternateDefaultValue: {alternateDefaultValue}"); + } + } } if (methodInfo is MethodArgumentInfo info) @@ -772,4 +783,11 @@ private void CheckIgnoredDictionaryAttribute(ISymbol member, bool isDictionary, _context.ReportDiagnostic(Diagnostics.IgnoredAttributeForDictionaryWithConverter(member, attribute)); } } + + private string? GetInitializerValue(IPropertySymbol symbol) + { + var syntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(_context.CancellationToken) as PropertyDeclarationSyntax; + var value = syntax?.Initializer?.Value as LiteralExpressionSyntax; + return value?.Token.ToFullString(); + } } diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 4c6b8566..406c7ba1 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -592,3 +592,16 @@ partial class DerivedArguments : BaseArguments [CommandLineArgument] public int DerivedArg { get; set; } } + +[GeneratedParser] +partial class InitializerDefaultValueArguments +{ + [CommandLineArgument] + public string Arg1 { get; set; } = "foo\tbar\""; + + [CommandLineArgument] + public float Arg2 { get; set; } = 5.5f; + + [CommandLineArgument] + public int Arg3 { get; set; } = int.MaxValue; +} diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 2ab7bf92..501d4444 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1144,6 +1144,16 @@ public void TestDerivedClass(ProviderKind kind) }); } + [TestMethod] + public void TestInitializerDefaultValues() + { + var parser = InitializerDefaultValueArguments.CreateParser(); + Assert.AreEqual("foo\tbar\"", parser.GetArgument("Arg1").DefaultValue); + Assert.AreEqual(5.5f, parser.GetArgument("Arg2").DefaultValue); + // Arg3's default value can't be used because it's not a literal. + Assert.IsNull(parser.GetArgument("Arg3").DefaultValue); + } + private class ExpectedArgument { public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind.SingleValue) diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 468b1674..15d55d94 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -49,6 +49,7 @@ private GeneratedArgument(ArgumentInfo info, Action? setPropert /// /// /// + /// /// /// /// @@ -73,6 +74,7 @@ public static GeneratedArgument Create(CommandLineParser parser, string defaultValueDescription, string? defaultKeyDescription = null, bool requiredProperty = false, + object? alternateDefaultValue = null, Type? keyType = null, Type? valueType = null, MultiValueSeparatorAttribute? multiValueSeparatorAttribute = null, @@ -95,6 +97,7 @@ public static GeneratedArgument Create(CommandLineParser parser, info.ElementTypeWithNullable = elementTypeWithNullable; info.Converter = converter; info.Kind = kind; + info.DefaultValue ??= alternateDefaultValue; if (info.Kind == ArgumentKind.Dictionary) { info.KeyValueSeparator ??= KeyValuePairConverter.DefaultSeparator; diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 52447637..18248e81 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -33,7 +33,7 @@ partial class Arguments : ICommand [Description("Test argument")] [Alias("t")] [ValidateNotEmpty] - public string? Test { get; set; } + public string? Test { get; set; } = "Hello"; [CommandLineArgument(Position = 1)] [ValueDescription("Stuff")] @@ -45,7 +45,7 @@ partial class Arguments : ICommand public int Test3 { get; set; } [CommandLineArgument] - public int? Test4 { get; set; } + public int? Test4 { get; set; } = 5; [CommandLineArgument] public FileInfo[]? File { get; set; } From 36a06ce263ca62ddf8654de806e10dc0a7ea96fe Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 22 May 2023 13:34:21 -0700 Subject: [PATCH 077/234] Source generation documentation updates. --- docs/DefiningArguments.md | 8 +- docs/SourceGeneration.md | 122 ++++--- docs/SourceGenerationDiagnostics.md | 340 ++++++++++++++++++ .../Properties/Resources.Designer.cs | 2 +- .../Properties/Resources.resx | 2 +- 5 files changed, 428 insertions(+), 46 deletions(-) create mode 100644 docs/SourceGenerationDiagnostics.md diff --git a/docs/DefiningArguments.md b/docs/DefiningArguments.md index 3142bec5..6631e40a 100644 --- a/docs/DefiningArguments.md +++ b/docs/DefiningArguments.md @@ -127,8 +127,9 @@ 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 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] @@ -138,7 +139,8 @@ public ICollection AlsoMultiValue { get; } = new List(); 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. +elements if the property type is either an array or [`ICollection`][] itself. This limitation +does not apply if [source generation](SourceGeneration.md) is used. ### Dictionary arguments diff --git a/docs/SourceGeneration.md b/docs/SourceGeneration.md index f8b45460..93c2f595 100644 --- a/docs/SourceGeneration.md +++ b/docs/SourceGeneration.md @@ -1,15 +1,12 @@ # Source generation -Ookii.CommandLine provides the option to use compile-time source generation to create a -`CommandLineParser` for an arguments type, and to create a `CommandManager`. Source generation -is only available for C# projects. +Ookii.CommandLine includes a source generator that can be used to generate a `CommandLineParser` +for an arguments type, or a `CommandManager` for the commands in an assembly, at compile time. The +source generator will generate C# code that creates those classes using information about your +arguments or command types available during compilation, rather than determining that information at +runtime using reflection. -## Generating a parser - -Normally, the `CommandLineParser` class uses runtime reflection to determine the command line -arguments defined by an arguments class. Instead, you can apply the `GeneratedParserAttribute` to -a class, which will use compile-time C# source generation to determine the arguments instead. This -approach has the following advantages: +Using source generation has several benefits: - Get [errors and warnings](TODO) at compile time for argument rule violations (such as a required positional argument after an optional positional argument), ignored options (such as setting a @@ -18,29 +15,34 @@ approach has the following advantages: `CommandLineArgumentAttribute` on a private or read-only property). These would normally be silently ignored or cause a runtime exception, but now you can catch problems during compilation. - Allow your application to be - [trimmed](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options). The way - Ookii.CommandLine uses reflection prevents trimming entirely. + [trimmed](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options). When + source generation is not used, the way Ookii.CommandLine uses reflection prevents trimming + entirely. - Improved performance. Benchmarks show that instantiating a `CommandLineParser` using a generated parser is up to thirty times faster than using reflection. However, since we're still - talking about microseconds, this is unlikely to matter much unless you're creating instances in a - loop for some reason. + talking about microseconds, this is unlikely to matter that much to a typical application. -Generally, it's recommended to use source generation unless you have a reason not to. Source -generation puts the following constraints on application: +A few restrictions apply to projects that use Ookii.ComandLine's source generation: -- The arguments class must be in a project using C# 8 or later. -- The project must be compiled using a recent version of the .Net SDK (TODO: Exact version). You - can target older runtimes supported by Ookii.CommandLine, down to .Net Framework 4.6, but you - must build the project using an SDK that supports the source generator. +- The project must a C# project (other languages are not supported), using C# version 8 or later. +- The project must be built using a recent version of the .Net SDK (TODO: Exact version). + - You can still target older runtimes supported by Ookii.CommandLine, down to .Net Framework 4.6, + but you must build the project using an SDK that supports the source generator, and set the + appropriate language version using the `` property in your project file. - If you use the `ArgumentConverterAttribute`, you must use the constructor that takes a `Type` instance. The constructor that takes a string is not supported. -- The arguments class may not be nested in another type. -- The arguments class may not be a generic type. +- The arguments or command manager class may not be nested in another type. +- The arguments or command manager class may not have generic type parameters. + +Generally, it's recommended to use source generation unless you cannot meet these requirements, or +you have another reason why you cannot use it. -Other than that, source generation offers all the same features that reflection does. +## Generating a parser -To generate a parser, you must mark the class as `partial`, and use the `GeneratedParserAttribute` -on the class. +Normally, the `CommandLineParser` class uses runtime reflection to determine the command line +arguments defined by an arguments class. To use source generation instead, use the +`GeneratedParserAttribute` attribute on your arguments class. You must also mark the class as +`partial`, because the source generator will add additional members to your class. ```csharp [GeneratedParser] @@ -51,6 +53,27 @@ partial class Arguments } ``` +The source generator will inspect the members and attributes of the class, and generates C# code +that provides that information to a `CommandLineParser`, without needing to use reflection. While +doing so, it checks whether your class violates any rules for defining arguments, and +[emits warnings and errors](TODO) if it does. + +If any of the arguments has a type for which there is no built-in `ArgumentConverter` class, and +the argument doesn't use the `ArgumentConverterAttribute`, the source generator will check whether +the type supports any of the standard methods of [argument value conversion](Arguments.md#argument-value-conversion), +and if it does, it will generate an `ArgumentConverter` implementation for that type (without +source generation, conversion for these types would normally also use reflection), and uses it +for the argument. + +Generated `ArgumentConverter` classes are internal to your project, and placed in the `Ookii.CommandLine.Conversion.Generated` +namespace. The namespace can be customized using the `GeneratedConverterNamespaceAttribute` +attribute. + +You can view any of the generated files using Visual Studio by looking under Dependencies, +Analyzers, Ookii.CommandLine.Generator in the Solution Explorer, or by setting the +`` property to true in your project file, in which case the generated +files will be placed under the `obj` folder of your project. + ### Using a generated parser When using the `GeneratedParserAttribute`, you must *not* use the regular `CommandLineParser` or @@ -62,8 +85,8 @@ still use reflection, even if a generated parser is available for a class. > was not intended. If for some reason you need to use reflection on a class that has that > attribute, you can set the `ParseOptions.AllowReflectionWithGeneratedParser` property to `true`. -Instead, the source generator will add the following methods to the arguments class (where -`Arguments` is the name of your class): +Instead, you should use one of the methods that the source generator will add to your arguments +class (where `Arguments` is the name of your class): ```csharp public static CommandLineParser CreateParser(ParseOptions? options = null); @@ -75,13 +98,6 @@ public static Arguments? Parse(string[] args, ParseOptions? options = null); public static Arguments? Parse(string[] args, int index, ParseOptions? options = null); ``` -If your project target .Net 7 or later, these methods will implement the `IParserProvider` -and `IParser` interfaces. - -Generating the `Parse()` methods is optional and can be disabled using the -`GeneratedParserAttribute.GenerateParseMethods` property. The `CreateParser()` method is always -generated. - Use the `CreateParser()` method as an alternative to the `CommandLineParser` constructor, and the `Parse()` methods as an alternative to the static `CommandLineParser.Parse()` methods. @@ -97,17 +113,19 @@ You would replace it with the following: var arguments = Arguments.Parse(); ``` +If your project targets .Net 7 or later, the generated class will implement the `IParserProvider` +and `IParser` interfaces, which define these methods. + +Generating the `Parse()` methods is optional, and can be disabled using the +`GeneratedParserAttribute.GenerateParseMethods` property. The `CreateParser()` method is always +generated. + ## Generating a command manager Just like the `CommandLineParser` class, the `CommandManager` class normally uses reflection to locate all command classes in the assembly or assemblies you specify. Instead, you can create a class with the `GeneratedCommandManagerAttribute` which can perform this same job at compile time. -Using a generated command manager has the same benefits and restrictions as a generated parser, -with one additional caveat: a generated command manager can only use commands defined in the same -assembly as the manager (TODO: update if changed). If you use commands from different or multiple -assemblies, you must continue to use the reflection method. - To create a generated command manager, define a partial class with the `GeneratedCommandManagerAttribute`: @@ -118,13 +136,35 @@ partial class MyCommandManager } ``` -The source generator will make it so that the class derives from `CommandManager`, and add the +The source generator will find all command classes in your project, and generate C# code to provide +those arguments to the `CommandManager` without needing reflection. + +If you need to load commands from a different assembly, or multiple assemblies, you can use the +`GeneratedCommandManagerAttribute.AssemblyNames` property. This property can use either just the +name of the assembly, or the full assembly identity including version, culture and public key +token. + +```csharp +[GeneratedCommandManager(AssemblyNames = new[] { "MyCommandAssembly" })] +partial class MyCommandManager +{ +} +``` + +If you wish to use commands from an assembly that is dynamically loaded during runtime, you must +continue using reflection. + +### Using a generated command manager + +The source generator will add `CommandManager` as a base class to your class, and add the following constructor to the class: ```csharp public MyCommandManager(CommandOptions? options = null) ``` +Instead of instantiation the `CommandManager` class, you use your generated class instead. + If you had the following code before using source generation: ```csharp @@ -132,7 +172,7 @@ var manager = new CommandManager(); return manager.RunCommand() ?? 1; ``` -You would simply replace it with the following: +You would replace it with the following: ```csharp var manager = new MyCommandManager(); @@ -141,7 +181,7 @@ return manager.RunCommand() ?? 1; ### Commands with generated parsers -You can apply the `GeneratedParserAttribute` to a command class, and the generated command manager +You can apply the `GeneratedParserAttribute` to a command class, and a generated command manager will use the generated parser for that command. ```csharp diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md new file mode 100644 index 00000000..ad029dd4 --- /dev/null +++ b/docs/SourceGenerationDiagnostics.md @@ -0,0 +1,340 @@ +# Source generation diagnostics + +The [source generator](SourceGeneration.md) will analyze your arguments class to see if it +does anything unsupported by Ookii.CommandLine. Among others, it checks for things such as: + +- Whether [positional arguments](Arguments.md#positional-arguments) that are required or multi-value + arguments follow the rules for their ordering. +- Whether positional arguments have duplicate numbering. +- Arguments with types that cannot be converted from a string. +- Attribute or property combinations that are ignored. +- Using the `CommandLineArgument` with a private member, or a method with an incorrect signature. + +Without source generation, these mistakes would either lead to a runtime exception when creating the +`CommandLineParser` class, or would be silently ignored. With source generation, you can instead +catch any problems during compile time, which reduces the risk of bugs. + +Not all errors can be caught at compile time. For example, the source generator does not check for +duplicate argument names, because the `ParseOptions.ArgumentNameTransform` property can modify the +names, which makes this impossible to determine at compile time. + +## Errors + +### CL0001 + +The command line arguments or command manager type must be a reference type. + +A command line arguments type, or a type using the `GeneratedCommandManagerAttribute`, must be a +reference type, or class. Value types (or structures) cannot be used. + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +partial struct Arguments // ERROR: The type must be a class. +{ + [CommandLineAttribute] + public string? Argument { get; set; } +} +``` + +### CL0002 + +The command line arguments or command manager class must be partial. + +When using the `GeneratedParserAttribute` or `GeneratedCommandManagerAttribute`, the target type +must use the `partial` modifier. + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +class Arguments // ERROR: The class must be partial +{ + [CommandLineAttribute] + public string? Argument { get; set; } +} +``` + +### CL0003 + +The command line arguments or command manager class must not have any generic type arguments. + +When using the `GeneratedParserAttribute` or `GeneratedCommandManagerAttribute`, the target type +cannot be a generic type. + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +partial class Arguments // ERROR: The class must not be generic +{ + [CommandLineAttribute] + public T? Argument { get; set; } +} +``` + +### CL0004 + +The command line arguments or command manager class must not be nested in another type. + +When using the `GeneratedParserAttribute` or `GeneratedCommandManagerAttribute`, the target type +cannot be nested in another type. + +For example, the following code triggers this error: + +```csharp +class SomeClass +{ + [GeneratedParser] + public partial class Arguments // ERROR: The class must not be nested + { + [CommandLineAttribute] + public T? Argument { get; set; } + } +} +``` + +### CL0005 + +A multi-value argument defined by a property with an array type must use an array rank of one. +Arrays with different ranks are not supported. + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // ERROR: Argument using an array rank other than one. + [CommandLineAttribute] + public string[,]? Argument { get; set; } +} +``` + +### CL0006 + +A command line argument property must have a public set accessor. + +The only exceptions to this rule are [multi-value](DefiningArguments.md#multi-value-arguments) or +[dictionary](DefiningArguments.md#dictionary-arguments) arguments, which may use a read-only +property depending on their type. + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // ERROR: Property must use a public set accessor. + [CommandLineAttribute] + public string? Argument { get; private set; } +} +``` + +### CL0007 + +No command line argument converter exists for the argument's type. + +The argument uses a type (or in the case of a multi-value or dictionary argument, an element type) +that cannot be converted from a string using the [default rules for argument conversion](Arguments.md#argument-value-conversion), +and no custom `ArgumentConverter` was specified. + +To fix this error, either change the type of the argument, or create a custom `ArgumentConverter` +and use the `ArgumentConverterAttribute` on the argument. + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // ERROR: Argument type must have a converter + [CommandLineAttribute] + public Socket? Argument { get; set; } +} +``` + +### CL0008 + +A method argument must use a supported signature. + +When using a method to define an argument, only [specific signatures](DefiningArguments.md#using-methods) +are allowed. This error indicates the method is not using one of the supported signatures, for +example it has additional parameters, or is not static. + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // ERROR: the method must be static + [CommandLineAttribute] + public void Argument(string value); +} +``` + +### CL0009 + +Init accessors may only be used with required properties. + +A property that defines a command line argument may only use an `init` accessor if it also uses the +`required` keyword. It's not sufficient to mark the argument required; you must use a required +property (required properties requires .Net 7 or later). + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // ERROR: The property uses init but is not required + [CommandLineAttribute(IsRequired = true)] + public string? Argument { get; init; } +} +``` + +To fix this error, either use a regular `set` accessor, or if using .Net 7.0 or later, use the +`required` keyword (setting `IsRequired` is not necessary in this case): + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute] + public required string Argument { get; init; } +} +``` + +### CL0010 + +The `GeneratedParserAttribute` cannot be used with a class that implements the +`ICommandWithCustomParsing` interface. + +The `ICommandWIthCustomParsing` interface is for commands that do not use the `CommandLineParser` +class, so a generated parser would not be used. + +For example, the following code triggers this error: + +TODO: Update with span/memory if used. + +```csharp +[Command] +[GeneratedParser] +partial class Arguments : ICommandWithCustomParsing // ERROR: The command uses custom parsing. +{ + public void Parse(string[] args, int index, CommandOptions options) + { + // Omitted + } + + public string Value { get; set; } + + public int Run() + { + // Omitted + } +} +``` + +### CL0011 + +A positional multi-value argument must be the last positional argument. + +If you use a multi-value argument as a positional argument, there cannot be any additional +positional arguments after that one. + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute(Position = 0)] + public string[]? Argument1 { get; set; } + + // ERROR: Argument2 comes after Argument1, which is multi-value. + [CommandLineAttribute(Position = 1)] + public string? Argument2 { get; set; } +} +``` + +### CL0012 + +Required positional arguments must come before optional positional arguments. + +If you have an optional positional argument, it must come after any required ones. + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute(Position = 0)] + public string? Argument1 { get; set; } + + // ERROR: Required argument Argument2 comes after Argument1, which is optional. + [CommandLineAttribute(IsRequired = true, Position = 1)] + public string? Argument2 { get; set; } +} +``` + +### CL0013 + +One of the assembly names specified in the `GeneratedCommandManagerAttribute.AssemblyNames` property +is not valid. This error is used when you give the full assembly identify, but it cannot be parsed. + +For example, the following code triggers this error: + +```csharp +// ERROR: The assembly name has an extra comma +[GeneratedCommandManager(AssemblyNames = new[] { "SomeAssembly,, Version=1.0.0.0" })] +partial class MyCommandManager +{ +} +``` + +### CL0014 + +One of the assembly names specified in the `GeneratedCommandManagerAttribute.AssemblyNames` property +could not be resolved. Make sure it's an assembly that is referenced by the current project. + +If you wish to load commands from an assembly that is not directly referenced by your project, you +must use the regular `CommandManager` class, using reflection instead of source generation, instead. + +For example, the following code triggers this error: + +```csharp +// ERROR: The assembly isn't referenced +[GeneratedCommandManager(AssemblyNames = new[] { "UnreferencedAssembly" })] +partial class MyCommandManager +{ +} +``` + +### CL0015 + +The `ArgumentConverterAttribute` must use the `typeof` keyword. + +The `ArgumentConverterAttribute` has two constructors, one that takes the `Type` of a converter, +and one that takes the name of a converter type as a string. The string constructor is not supported +when using source generation. + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute] + [ArgumentConverter("MyNamespace.MyConverter")] // ERROR: Can't use a string type name. + public CustomType? Argument { get; set; } +} +``` + +To fix this error, either use the constructor that takes a `Type` using the `typeof` keyword, or +use a `CommandLineParser` without using source generation. + +## Warnings + +TODO diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 5331829c..8c874c23 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -565,7 +565,7 @@ internal static string TypeNotReferenceTypeMessageFormat { } /// - /// Looks up a localized string similar to The command line arguments type must be a reference type.. + /// 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 { diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index a2dfedd3..3e53b8de 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -286,7 +286,7 @@ The type {0} must be a reference type (class) when the {1} attribute is used. - The command line arguments type must be a reference type. + The command line arguments or command manager type must be a reference type. An assembly matching the name '{0}' was not found. From 6cb6427773b2b4ecf745dc0a5b6cd17f13b9d76e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 22 May 2023 13:39:47 -0700 Subject: [PATCH 078/234] Fix typos. --- docs/SourceGenerationDiagnostics.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index ad029dd4..d223f155 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -181,7 +181,7 @@ Init accessors may only be used with required properties. A property that defines a command line argument may only use an `init` accessor if it also uses the `required` keyword. It's not sufficient to mark the argument required; you must use a required -property (required properties requires .Net 7 or later). +property (required properties are only supported on .Net 7 or later). For example, the following code triggers this error: @@ -229,8 +229,6 @@ partial class Arguments : ICommandWithCustomParsing // ERROR: The command uses c // Omitted } - public string Value { get; set; } - public int Run() { // Omitted From fbecf3d8de52b569399cffa2b0f50cd8d2830de1 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 25 May 2023 16:45:12 -0700 Subject: [PATCH 079/234] Warn for argument names starting with numbers. --- .../CommandLineArgumentAttributeInfo.cs | 7 +++++++ src/Ookii.CommandLine.Generator/Diagnostics.cs | 9 +++++++++ .../ParserGenerator.cs | 12 ++++++++++++ .../Properties/Resources.Designer.cs | 18 ++++++++++++++++++ .../Properties/Resources.resx | 6 ++++++ 5 files changed, 52 insertions(+) diff --git a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs index 990d7e7f..fede2bce 100644 --- a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs +++ b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs @@ -8,6 +8,11 @@ internal class CommandLineArgumentAttributeInfo public CommandLineArgumentAttributeInfo(AttributeData data) { + if (data.ConstructorArguments.Length > 0) + { + ArgumentName = data.ConstructorArguments[0].Value as string; + } + foreach (var named in data.NamedArguments) { switch (named.Key) @@ -49,6 +54,8 @@ public CommandLineArgumentAttributeInfo(AttributeData data) } } + public string? ArgumentName { get; } + public bool IsRequired { get; } public bool HasIsRequired { get; } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 19460244..3a79870c 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -277,6 +277,15 @@ public static Diagnostic IgnoredAttributeForNonMultiValue(ISymbol member, Attrib attribute.AttributeClass?.Name, member.ToDisplayString()); + public static Diagnostic ArgumentStartsWithNumber(ISymbol member, string name) => CreateDiagnostic( + "CLW0013", + nameof(Resources.ArgumentStartsWithNumberTitle), + nameof(Resources.ArgumentStartsWithNumberMessageFormat), + DiagnosticSeverity.Warning, + member.Locations.FirstOrDefault(), + name, + member.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 546945ba..a07c426a 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -520,6 +520,18 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> }); } + // 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)); diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 8c874c23..46388d55 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -96,6 +96,24 @@ internal static string ArgumentConverterStringNotSupportedTitle { } } + /// + /// 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 class {0} may not be a generic class when the {1} attribute is used.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 3e53b8de..1067a59f 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -129,6 +129,12 @@ 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 class {0} may not be a generic class when the {1} attribute is used. From 4a1f44541cb363a2cc1298d16e0ba311fef304d9 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 25 May 2023 17:11:22 -0700 Subject: [PATCH 080/234] Cleaner function argument generation. --- .../CommandGenerator.cs | 18 +++--- .../ParserGenerator.cs | 57 +++++++++---------- .../SourceBuilder.cs | 27 +++++++++ 3 files changed, 64 insertions(+), 38 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index 58910770..356b7f7e 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -171,38 +171,38 @@ private void GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType } builder.IncreaseIndent(); - builder.AppendLine("manager"); + builder.AppendArgument("manager"); if (!useCustomParsing) { - builder.AppendLine($", typeof({commandTypeName})"); + builder.AppendArgument($"typeof({commandTypeName})"); } var attributes = commandAttributes ?? new ArgumentsClassAttributes(commandType, _typeHelper, _context); - builder.AppendLine($", {attributes.Command!.CreateInstantiation()}"); + builder.AppendArgument($"{attributes.Command!.CreateInstantiation()}"); if (attributes.Description != null) { - builder.AppendLine($", descriptionAttribute: {attributes.Description.CreateInstantiation()}"); + builder.AppendArgument($"descriptionAttribute: {attributes.Description.CreateInstantiation()}"); } if (attributes.Aliases != null) { - builder.AppendLine($", aliasAttributes: new Ookii.CommandLine.AliasAttribute[] {{ {string.Join(", ", attributes.Aliases.Select(a => a.CreateInstantiation()))} }}"); + builder.AppendArgument($"aliasAttributes: new Ookii.CommandLine.AliasAttribute[] {{ {string.Join(", ", attributes.Aliases.Select(a => a.CreateInstantiation()))} }}"); } if (!useCustomParsing) { if (attributes.GeneratedParser != null) { - builder.AppendLine($", createParser: options => {commandTypeName}.CreateParser(options)"); + builder.AppendArgument($"createParser: options => {commandTypeName}.CreateParser(options)"); } else { - builder.AppendLine($", createParser: options => new CommandLineParser<{commandTypeName}>(options)"); + builder.AppendArgument($"createParser: options => new CommandLineParser<{commandTypeName}>(options)"); } } - builder.DecreaseIndent(); - builder.AppendLine(");"); + builder.CloseArgumentList(); + builder.AppendLine(); } private IEnumerable<(INamedTypeSymbol, ArgumentsClassAttributes?)>? GetCommands(string? assemblyName, ITypeSymbol manager) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index a07c426a..194141f0 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -144,13 +144,12 @@ private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isComman _builder.IncreaseIndent(); _builder.AppendLine(": base("); _builder.IncreaseIndent(); - _builder.AppendLine($"typeof({_argumentsClass.Name})"); + _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.DecreaseIndent(); - _builder.AppendLine(")"); + _builder.CloseArgumentList(false); _builder.DecreaseIndent(); _builder.AppendLine("{}"); _builder.AppendLine(); @@ -383,30 +382,30 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> // 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.AppendLine("parser"); - _builder.AppendLine($", argumentType: typeof({argumentType.ToDisplayString()})"); - _builder.AppendLine($", elementTypeWithNullable: typeof({elementTypeWithNullable.ToDisplayString()})"); - _builder.AppendLine($", elementType: typeof({elementType.ToDisplayString()})"); - _builder.AppendLine($", memberName: \"{member.Name}\""); - _builder.AppendLine($", kind: {kind}"); - _builder.AppendLine($", attribute: {attributes.CommandLineArgument.CreateInstantiation()}"); - _builder.AppendLine($", converter: {converter}"); - _builder.AppendLine($", allowsNull: {(allowsNull.ToCSharpString())}"); + _builder.AppendArgument("parser"); + _builder.AppendArgument($"argumentType: typeof({argumentType.ToDisplayString()})"); + _builder.AppendArgument($"elementTypeWithNullable: typeof({elementTypeWithNullable.ToDisplayString()})"); + _builder.AppendArgument($"elementType: typeof({elementType.ToDisplayString()})"); + _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.AppendLine($", keyType: typeof({keyType.ToDisplayString()})"); - _builder.AppendLine($", defaultKeyDescription: \"{keyType.ToDisplayString(valueDescriptionFormat)}\""); + _builder.AppendArgument($"keyType: typeof({keyType.ToDisplayString()})"); + _builder.AppendArgument($"defaultKeyDescription: \"{keyType.ToDisplayString(valueDescriptionFormat)}\""); } if (valueType != null) { - _builder.AppendLine($", valueType: typeof({valueType.ToDisplayString()})"); - _builder.AppendLine($", defaultValueDescription: \"{valueType.ToDisplayString(valueDescriptionFormat)}\""); + _builder.AppendArgument($"valueType: typeof({valueType.ToDisplayString()})"); + _builder.AppendArgument($"defaultValueDescription: \"{valueType.ToDisplayString(valueDescriptionFormat)}\""); } else { - _builder.AppendLine($", defaultValueDescription: \"{elementType.ToDisplayString(valueDescriptionFormat)}\""); + _builder.AppendArgument($"defaultValueDescription: \"{elementType.ToDisplayString(valueDescriptionFormat)}\""); } AppendOptionalAttribute(attributes.MultiValueSeparator, "multiValueSeparatorAttribute"); @@ -414,12 +413,12 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> AppendOptionalAttribute(attributes.ValueDescription, "valueDescriptionAttribute"); if (attributes.AllowDuplicateDictionaryKeys != null) { - _builder.AppendLine(", allowDuplicateDictionaryKeys: true"); + _builder.AppendArgument("allowDuplicateDictionaryKeys: true"); } if (attributes.KeyValueSeparator != null) { - _builder.AppendLine($", keyValueSeparatorAttribute: keyValueSeparatorAttribute{member.Name}"); + _builder.AppendArgument($"keyValueSeparatorAttribute: keyValueSeparatorAttribute{member.Name}"); } AppendOptionalAttribute(attributes.Aliases, "aliasAttributes", "Ookii.CommandLine.AliasAttribute"); @@ -429,11 +428,11 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> { if (property.SetMethod != null && property.SetMethod.DeclaredAccessibility == Accessibility.Public && !property.SetMethod.IsInitOnly) { - _builder.AppendLine($", setProperty: (target, value) => (({_argumentsClass.ToDisplayString()})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"); + _builder.AppendArgument($"setProperty: (target, value) => (({_argumentsClass.ToDisplayString()})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"); } - _builder.AppendLine($", getProperty: (target) => (({_argumentsClass.ToDisplayString()})target).{member.Name}"); - _builder.AppendLine($", requiredProperty: {property.IsRequired.ToCSharpString()}"); + _builder.AppendArgument($"getProperty: (target) => (({_argumentsClass.ToDisplayString()})target).{member.Name}"); + _builder.AppendArgument($"requiredProperty: {property.IsRequired.ToCSharpString()}"); if (argumentInfo.DefaultValue != null) { if (isMultiValue) @@ -457,7 +456,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> var alternateDefaultValue = GetInitializerValue(property); if (alternateDefaultValue != null) { - _builder.AppendLine($", alternateDefaultValue: {alternateDefaultValue}"); + _builder.AppendArgument($"alternateDefaultValue: {alternateDefaultValue}"); } } } @@ -483,11 +482,11 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> if (info.HasBooleanReturn) { - _builder.AppendLine($", callMethod: (value, parser) => {_argumentsClass.ToDisplayString()}.{member.Name}({arguments})"); + _builder.AppendArgument($"callMethod: (value, parser) => {_argumentsClass.ToDisplayString()}.{member.Name}({arguments})"); } else { - _builder.AppendLine($", callMethod: (value, parser) => {{ {_argumentsClass.ToDisplayString()}.{member.Name}({arguments}); return true; }}"); + _builder.AppendArgument($"callMethod: (value, parser) => {{ {_argumentsClass.ToDisplayString()}.{member.Name}({arguments}); return true; }}"); } if (argumentInfo.DefaultValue != null) @@ -496,8 +495,8 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> } } - _builder.DecreaseIndent(); - _builder.AppendLine(");"); + _builder.CloseArgumentList(); + _builder.AppendLine(); if (argumentInfo.Position is int position) { _positions ??= new(); @@ -727,7 +726,7 @@ private void AppendOptionalAttribute(AttributeData? attribute, string name) { if (attribute != null) { - _builder.AppendLine($", {name}: {attribute.CreateInstantiation()}"); + _builder.AppendArgument($"{name}: {attribute.CreateInstantiation()}"); } } @@ -735,7 +734,7 @@ private void AppendOptionalAttribute(List? attributes, string nam { if (attributes != null) { - _builder.AppendLine($", {name}: new {typeName}[] {{ {string.Join(", ", attributes.Select(a => a.CreateInstantiation()))} }}"); + _builder.AppendArgument($"{name}: new {typeName}[] {{ {string.Join(", ", attributes.Select(a => a.CreateInstantiation()))} }}"); } } diff --git a/src/Ookii.CommandLine.Generator/SourceBuilder.cs b/src/Ookii.CommandLine.Generator/SourceBuilder.cs index deb9f09b..be75d029 100644 --- a/src/Ookii.CommandLine.Generator/SourceBuilder.cs +++ b/src/Ookii.CommandLine.Generator/SourceBuilder.cs @@ -8,6 +8,7 @@ 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()) @@ -46,6 +47,32 @@ public void AppendLine(string 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("{"); From 0ffd4072c4f2dadc2304c963c41a18e31abb7f05 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 25 May 2023 17:20:30 -0700 Subject: [PATCH 081/234] Renumbered diagnostics. --- docs/SourceGenerationDiagnostics.md | 30 ++++----- .../Diagnostics.cs | 67 +++++++++---------- 2 files changed, 47 insertions(+), 50 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index d223f155..342aba72 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -20,7 +20,7 @@ names, which makes this impossible to determine at compile time. ## Errors -### CL0001 +### OCL0001 The command line arguments or command manager type must be a reference type. @@ -38,7 +38,7 @@ partial struct Arguments // ERROR: The type must be a class. } ``` -### CL0002 +### OCL0002 The command line arguments or command manager class must be partial. @@ -56,7 +56,7 @@ class Arguments // ERROR: The class must be partial } ``` -### CL0003 +### OCL0003 The command line arguments or command manager class must not have any generic type arguments. @@ -74,7 +74,7 @@ partial class Arguments // ERROR: The class must not be generic } ``` -### CL0004 +### OCL0004 The command line arguments or command manager class must not be nested in another type. @@ -95,7 +95,7 @@ class SomeClass } ``` -### CL0005 +### OCL0005 A multi-value argument defined by a property with an array type must use an array rank of one. Arrays with different ranks are not supported. @@ -112,7 +112,7 @@ partial class Arguments } ``` -### CL0006 +### OCL0006 A command line argument property must have a public set accessor. @@ -132,7 +132,7 @@ partial class Arguments } ``` -### CL0007 +### OCL0007 No command line argument converter exists for the argument's type. @@ -155,7 +155,7 @@ partial class Arguments } ``` -### CL0008 +### OCL0008 A method argument must use a supported signature. @@ -175,7 +175,7 @@ partial class Arguments } ``` -### CL0009 +### OCL0009 Init accessors may only be used with required properties. @@ -207,7 +207,7 @@ partial class Arguments } ``` -### CL0010 +### OCL0010 The `GeneratedParserAttribute` cannot be used with a class that implements the `ICommandWithCustomParsing` interface. @@ -236,7 +236,7 @@ partial class Arguments : ICommandWithCustomParsing // ERROR: The command uses c } ``` -### CL0011 +### OCL0011 A positional multi-value argument must be the last positional argument. @@ -258,7 +258,7 @@ partial class Arguments } ``` -### CL0012 +### OCL0012 Required positional arguments must come before optional positional arguments. @@ -279,7 +279,7 @@ partial class Arguments } ``` -### CL0013 +### OCL0013 One of the assembly names specified in the `GeneratedCommandManagerAttribute.AssemblyNames` property is not valid. This error is used when you give the full assembly identify, but it cannot be parsed. @@ -294,7 +294,7 @@ partial class MyCommandManager } ``` -### CL0014 +### OCL0014 One of the assembly names specified in the `GeneratedCommandManagerAttribute.AssemblyNames` property could not be resolved. Make sure it's an assembly that is referenced by the current project. @@ -312,7 +312,7 @@ partial class MyCommandManager } ``` -### CL0015 +### OCL0015 The `ArgumentConverterAttribute` must use the `typeof` keyword. diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 3a79870c..14d22465 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -1,8 +1,5 @@ using Microsoft.CodeAnalysis; using Ookii.CommandLine.Generator.Properties; -using System; -using System.Collections.Generic; -using System.Text; namespace Ookii.CommandLine.Generator; @@ -11,7 +8,7 @@ internal static class Diagnostics private const string Category = "Ookii.CommandLine"; public static Diagnostic TypeNotReferenceType(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( - "CL0001", + "OCL0001", nameof(Resources.TypeNotReferenceTypeTitle), nameof(Resources.TypeNotReferenceTypeMessageFormat), DiagnosticSeverity.Error, @@ -20,7 +17,7 @@ public static Diagnostic TypeNotReferenceType(INamedTypeSymbol symbol, string at attributeName); public static Diagnostic ClassNotPartial(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( - "CL0002", + "OCL0002", nameof(Resources.ClassNotPartialTitle), nameof(Resources.ClassNotPartialMessageFormat), DiagnosticSeverity.Error, @@ -29,7 +26,7 @@ public static Diagnostic ClassNotPartial(INamedTypeSymbol symbol, string attribu attributeName); public static Diagnostic ClassIsGeneric(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( - "CL0003", + "OCL0003", nameof(Resources.ClassIsGenericTitle), nameof(Resources.ClassIsGenericMessageFormat), DiagnosticSeverity.Error, @@ -38,7 +35,7 @@ public static Diagnostic ClassIsGeneric(INamedTypeSymbol symbol, string attribut attributeName); public static Diagnostic ClassIsNested(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( - "CL0004", + "OCL0004", nameof(Resources.ClassIsNestedTitle), nameof(Resources.ClassIsNestedMessageFormat), DiagnosticSeverity.Error, @@ -48,7 +45,7 @@ public static Diagnostic ClassIsNested(INamedTypeSymbol symbol, string attribute public static Diagnostic InvalidArrayRank(IPropertySymbol property) => CreateDiagnostic( - "CL0005", + "OCL0005", nameof(Resources.InvalidArrayRankTitle), nameof(Resources.InvalidArrayRankMessageFormat), DiagnosticSeverity.Error, @@ -57,7 +54,7 @@ public static Diagnostic InvalidArrayRank(IPropertySymbol property) => CreateDia property.Name); public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => CreateDiagnostic( - "CL0006", + "OCL0006", nameof(Resources.PropertyIsReadOnlyTitle), nameof(Resources.PropertyIsReadOnlyMessageFormat), DiagnosticSeverity.Error, @@ -66,7 +63,7 @@ public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => CreateD property.Name); public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => CreateDiagnostic( - "CL0007", + "OCL0007", nameof(Resources.NoConverterTitle), nameof(Resources.NoConverterMessageFormat), DiagnosticSeverity.Error, @@ -76,7 +73,7 @@ public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => member.Name); public static Diagnostic InvalidMethodSignature(ISymbol method) => CreateDiagnostic( - "CL0008", + "OCL0008", nameof(Resources.InvalidMethodSignatureTitle), nameof(Resources.InvalidMethodSignatureMessageFormat), DiagnosticSeverity.Error, @@ -85,7 +82,7 @@ public static Diagnostic InvalidMethodSignature(ISymbol method) => CreateDiagnos method.Name); public static Diagnostic NonRequiredInitOnlyProperty(IPropertySymbol property) => CreateDiagnostic( - "CL0009", + "OCL0009", nameof(Resources.NonRequiredInitOnlyPropertyTitle), nameof(Resources.NonRequiredInitOnlyPropertyMessageFormat), DiagnosticSeverity.Error, @@ -94,7 +91,7 @@ public static Diagnostic NonRequiredInitOnlyProperty(IPropertySymbol property) = property.Name); public static Diagnostic GeneratedCustomParsingCommand(INamedTypeSymbol symbol) => CreateDiagnostic( - "CL0010", + "OCL0010", nameof(Resources.GeneratedCustomParsingCommandTitle), nameof(Resources.GeneratedCustomParsingCommandMessageFormat), DiagnosticSeverity.Error, @@ -102,7 +99,7 @@ public static Diagnostic GeneratedCustomParsingCommand(INamedTypeSymbol symbol) symbol.ToDisplayString()); public static Diagnostic PositionalArgumentAfterMultiValue(ISymbol symbol, string other) => CreateDiagnostic( - "CL0011", + "OCL0011", nameof(Resources.PositionalArgumentAfterMultiValueTitle), nameof(Resources.PositionalArgumentAfterMultiValueMessageFormat), DiagnosticSeverity.Error, @@ -111,7 +108,7 @@ public static Diagnostic PositionalArgumentAfterMultiValue(ISymbol symbol, strin other); public static Diagnostic PositionalRequiredArgumentAfterOptional(ISymbol symbol, string other) => CreateDiagnostic( - "CL0012", + "OCL0012", nameof(Resources.PositionalRequiredArgumentAfterOptionalTitle), nameof(Resources.PositionalRequiredArgumentAfterOptionalMessageFormat), DiagnosticSeverity.Error, @@ -120,7 +117,7 @@ public static Diagnostic PositionalRequiredArgumentAfterOptional(ISymbol symbol, other); public static Diagnostic InvalidAssemblyName(ISymbol symbol, string name) => CreateDiagnostic( - "CL0013", + "OCL0013", nameof(Resources.InvalidAssemblyNameTitle), nameof(Resources.InvalidAssemblyNameMessageFormat), DiagnosticSeverity.Error, @@ -128,7 +125,7 @@ public static Diagnostic InvalidAssemblyName(ISymbol symbol, string name) => Cre name); public static Diagnostic UnknownAssemblyName(ISymbol symbol, string name) => CreateDiagnostic( - "CL0014", + "OCL0014", nameof(Resources.UnknownAssemblyNameTitle), nameof(Resources.UnknownAssemblyNameMessageFormat), DiagnosticSeverity.Error, @@ -136,7 +133,7 @@ public static Diagnostic UnknownAssemblyName(ISymbol symbol, string name) => Cre name); public static Diagnostic ArgumentConverterStringNotSupported(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( - "CL0015", + "OCL0015", nameof(Resources.ArgumentConverterStringNotSupportedTitle), nameof(Resources.ArgumentConverterStringNotSupportedMessageFormat), DiagnosticSeverity.Error, @@ -144,7 +141,7 @@ public static Diagnostic ArgumentConverterStringNotSupported(AttributeData attri symbol.ToDisplayString()); public static Diagnostic IgnoredAttribute(AttributeData attribute) => CreateDiagnostic( - "CLW0001", + "OCL0016", nameof(Resources.UnknownAttributeTitle), nameof(Resources.UnknownAttributeMessageFormat), DiagnosticSeverity.Warning, @@ -152,7 +149,7 @@ public static Diagnostic IgnoredAttribute(AttributeData attribute) => CreateDiag attribute.AttributeClass?.Name); public static Diagnostic NonPublicStaticMethod(ISymbol method) => CreateDiagnostic( - "CLW0002", + "OCL0017", nameof(Resources.NonPublicStaticMethodTitle), nameof(Resources.NonPublicStaticMethodMessageFormat), DiagnosticSeverity.Warning, @@ -161,7 +158,7 @@ public static Diagnostic NonPublicStaticMethod(ISymbol method) => CreateDiagnost method.Name); public static Diagnostic NonPublicInstanceProperty(ISymbol property) => CreateDiagnostic( - "CLW0003", + "OCL0018", nameof(Resources.NonPublicInstancePropertyTitle), nameof(Resources.NonPublicInstancePropertyMessageFormat), DiagnosticSeverity.Warning, @@ -170,7 +167,7 @@ public static Diagnostic NonPublicInstanceProperty(ISymbol property) => CreateDi property.Name); public static Diagnostic CommandAttributeWithoutInterface(INamedTypeSymbol symbol) => CreateDiagnostic( - "CLW0004", + "OCL0019", nameof(Resources.CommandAttributeWithoutInterfaceTitle), nameof(Resources.CommandAttributeWithoutInterfaceMessageFormat), DiagnosticSeverity.Warning, @@ -178,7 +175,7 @@ public static Diagnostic CommandAttributeWithoutInterface(INamedTypeSymbol symbo symbol.ToDisplayString()); public static Diagnostic DefaultValueWithRequired(ISymbol symbol) => CreateDiagnostic( - "CLW0005", + "OCL0020", nameof(Resources.DefaultValueIgnoredTitle), nameof(Resources.DefaultValueWithRequiredMessageFormat), DiagnosticSeverity.Warning, @@ -186,7 +183,7 @@ public static Diagnostic DefaultValueWithRequired(ISymbol symbol) => CreateDiagn symbol.ToDisplayString()); public static Diagnostic DefaultValueWithMultiValue(ISymbol symbol) => CreateDiagnostic( - "CLW0005", // Deliberately the same as above. + "OCL0020", // Deliberately the same as above. nameof(Resources.DefaultValueIgnoredTitle), nameof(Resources.DefaultValueWithMultiValueMessageFormat), DiagnosticSeverity.Warning, @@ -194,7 +191,7 @@ public static Diagnostic DefaultValueWithMultiValue(ISymbol symbol) => CreateDia symbol.ToDisplayString()); public static Diagnostic DefaultValueWithMethod(ISymbol symbol) => CreateDiagnostic( - "CLW0005", // Deliberately the same as above. + "OCL0020", // Deliberately the same as above. nameof(Resources.DefaultValueIgnoredTitle), nameof(Resources.DefaultValueWithMethodMessageFormat), DiagnosticSeverity.Warning, @@ -202,7 +199,7 @@ public static Diagnostic DefaultValueWithMethod(ISymbol symbol) => CreateDiagnos symbol.ToDisplayString()); public static Diagnostic IsRequiredWithRequiredProperty(ISymbol symbol) => CreateDiagnostic( - "CLW0006", + "OCL0021", nameof(Resources.IsRequiredWithRequiredPropertyTitle), nameof(Resources.IsRequiredWithRequiredPropertyMessageFormat), DiagnosticSeverity.Warning, @@ -210,7 +207,7 @@ public static Diagnostic IsRequiredWithRequiredProperty(ISymbol symbol) => Creat symbol.ToDisplayString()); public static Diagnostic DuplicatePosition(ISymbol symbol, string otherName) => CreateDiagnostic( - "CLW0007", + "OCL0022", nameof(Resources.DuplicatePositionTitle), nameof(Resources.DuplicatePositionMessageFormat), DiagnosticSeverity.Warning, @@ -219,7 +216,7 @@ public static Diagnostic DuplicatePosition(ISymbol symbol, string otherName) => otherName); public static Diagnostic ShortAliasWithoutShortName(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( - "CLW0008", + "OCL0023", nameof(Resources.ShortAliasWithoutShortNameTitle), nameof(Resources.ShortAliasWithoutShortNameMessageFormat), DiagnosticSeverity.Warning, @@ -227,7 +224,7 @@ public static Diagnostic ShortAliasWithoutShortName(AttributeData attribute, ISy symbol.ToDisplayString()); public static Diagnostic AliasWithoutLongName(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( - "CLW0009", + "OCL0024", nameof(Resources.AliasWithoutLongNameTitle), nameof(Resources.AliasWithoutLongNameMessageFormat), DiagnosticSeverity.Warning, @@ -235,7 +232,7 @@ public static Diagnostic AliasWithoutLongName(AttributeData attribute, ISymbol s symbol.ToDisplayString()); public static Diagnostic IsHiddenWithPositional(ISymbol symbol) => CreateDiagnostic( - "CLW0010", + "OCL0025", nameof(Resources.IsHiddenWithPositionalTitle), nameof(Resources.IsHiddenWithPositionalMessageFormat), DiagnosticSeverity.Warning, @@ -243,7 +240,7 @@ public static Diagnostic IsHiddenWithPositional(ISymbol symbol) => CreateDiagnos symbol.ToDisplayString()); public static Diagnostic InvalidGeneratedConverterNamespace(string ns, AttributeData attribute) => CreateDiagnostic( - "CLW0010", + "OCL0026", nameof(Resources.InvalidGeneratedConverterNamespaceTitle), nameof(Resources.InvalidGeneratedConverterNamespaceMessageFormat), DiagnosticSeverity.Warning, @@ -251,7 +248,7 @@ public static Diagnostic InvalidGeneratedConverterNamespace(string ns, Attribute ns); public static Diagnostic IgnoredAttributeForNonDictionary(ISymbol member, AttributeData attribute) => CreateDiagnostic( - "CLW0011", + "OCL0027", nameof(Resources.IgnoredAttributeForNonDictionaryTitle), nameof(Resources.IgnoredAttributeForNonDictionaryMessageFormat), DiagnosticSeverity.Warning, @@ -260,7 +257,7 @@ public static Diagnostic IgnoredAttributeForNonDictionary(ISymbol member, Attrib member.ToDisplayString()); public static Diagnostic IgnoredAttributeForDictionaryWithConverter(ISymbol member, AttributeData attribute) => CreateDiagnostic( - "CLW0012", + "OCL0028", nameof(Resources.IgnoredAttributeForDictionaryWithConverterTitle), nameof(Resources.IgnoredAttributeForDictionaryWithConverterMessageFormat), DiagnosticSeverity.Warning, @@ -269,7 +266,7 @@ public static Diagnostic IgnoredAttributeForDictionaryWithConverter(ISymbol memb member.ToDisplayString()); public static Diagnostic IgnoredAttributeForNonMultiValue(ISymbol member, AttributeData attribute) => CreateDiagnostic( - "CLW0013", + "OCL0029", nameof(Resources.IgnoredAttributeForNonMultiValueTitle), nameof(Resources.IgnoredAttributeForNonMultiValueMessageFormat), DiagnosticSeverity.Warning, @@ -278,7 +275,7 @@ public static Diagnostic IgnoredAttributeForNonMultiValue(ISymbol member, Attrib member.ToDisplayString()); public static Diagnostic ArgumentStartsWithNumber(ISymbol member, string name) => CreateDiagnostic( - "CLW0013", + "OCL0030", nameof(Resources.ArgumentStartsWithNumberTitle), nameof(Resources.ArgumentStartsWithNumberMessageFormat), DiagnosticSeverity.Warning, From 99e4de167c487393b62266c93e0c9e1cc0135727 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 25 May 2023 18:15:53 -0700 Subject: [PATCH 082/234] Diagnostics documentation updates. --- docs/README.md | 1 + docs/SourceGeneration.md | 12 +- docs/SourceGenerationDiagnostics.md | 324 +++++++++++++++++++++++++++- 3 files changed, 324 insertions(+), 13 deletions(-) diff --git a/docs/README.md b/docs/README.md index cf734cb0..5511bfbc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,6 +20,7 @@ explanations of what they do and examples of their output. - [Argument validation and dependencies](Validation.md) - [Subcommands](Subcommands.md) - [Source generation](SourceGeneration.md) + - [Diagnostics](SourceGenerationDiagnostics.md) - [LineWrappingTextWriter and other utilities](Utilities.md) - [Code snippets](CodeSnippets.md) - [Class library documentation](https://www.ookii.org/Link/CommandLineDoc) diff --git a/docs/SourceGeneration.md b/docs/SourceGeneration.md index 93c2f595..a45e0406 100644 --- a/docs/SourceGeneration.md +++ b/docs/SourceGeneration.md @@ -8,11 +8,11 @@ runtime using reflection. Using source generation has several benefits: -- Get [errors and warnings](TODO) at compile time for argument rule violations (such as a required - positional argument after an optional positional argument), ignored options (such as setting a - default value for a required attribute), and other problems (such as using the same position - number more than once, method arguments with the wrong signature, or using the - `CommandLineArgumentAttribute` on a private or read-only property). These would normally be +- Get [errors and warnings](SourceGenerationDiagnostics.md) at compile time for argument rule + violations (such as a required positional argument after an optional positional argument), ignored + options (such as setting a default value for a required attribute), and other problems (such as + using the same position number more than once, method arguments with the wrong signature, or using + the `CommandLineArgumentAttribute` on a private or read-only property). These would normally be silently ignored or cause a runtime exception, but now you can catch problems during compilation. - Allow your application to be [trimmed](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options). When @@ -56,7 +56,7 @@ partial class Arguments The source generator will inspect the members and attributes of the class, and generates C# code that provides that information to a `CommandLineParser`, without needing to use reflection. While doing so, it checks whether your class violates any rules for defining arguments, and -[emits warnings and errors](TODO) if it does. +[emits warnings and errors](SourceGenerationDiagnostics.md) if it does. If any of the arguments has a type for which there is no built-in `ArgumentConverter` class, and the argument doesn't use the `ArgumentConverterAttribute`, the source generator will check whether diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 342aba72..fb1ee045 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -15,8 +15,8 @@ Without source generation, these mistakes would either lead to a runtime excepti catch any problems during compile time, which reduces the risk of bugs. Not all errors can be caught at compile time. For example, the source generator does not check for -duplicate argument names, because the `ParseOptions.ArgumentNameTransform` property can modify the -names, which makes this impossible to determine at compile time. +duplicate argument names, because the `ParseOptions.ArgumentNameTransform` property and +`ParseOptions.ArgumentNameComparison` properties can render the result of this check inaccurate. ## Errors @@ -160,8 +160,8 @@ partial class Arguments A method argument must use a supported signature. When using a method to define an argument, only [specific signatures](DefiningArguments.md#using-methods) -are allowed. This error indicates the method is not using one of the supported signatures, for -example it has additional parameters, or is not static. +are allowed. This error indicates the method is not using one of the supported signatures; for +example, it has additional parameters. For example, the following code triggers this error: @@ -169,9 +169,9 @@ For example, the following code triggers this error: [GeneratedParser] partial class Arguments { - // ERROR: the method must be static + // ERROR: the method has an unrecognized parameter [CommandLineAttribute] - public void Argument(string value); + public static void Argument(string value, int value2); } ``` @@ -320,6 +320,8 @@ The `ArgumentConverterAttribute` has two constructors, one that takes the `Type` and one that takes the name of a converter type as a string. The string constructor is not supported when using source generation. +For example, the following code triggers this error: + ```csharp [GeneratedParser] partial class Arguments @@ -335,4 +337,312 @@ use a `CommandLineParser` without using source generation. ## Warnings -TODO +### OCL0016 + +Unknown attribute will be ignored. + +The arguments class itself, or one of the members defining an argument, has an attribute that is +not used by Ookii.CommandLine. + +For example, the following code triggers this warning, because the current version of +Ookii.CommandLine no longer uses the `TypeConverterAttribute`, having replaced it with the +`ArgumentConverterAttribute`: + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute] + [TypeConverter(typeof(MyNamespace.MyConverter)] // WARNING: TypeConverterAttribute is not used + public CustomType? Argument { get; set; } +} +``` + +To fix this warning, remove the relevant attribute. If the attribute is present for some purpose +other than Ookii.CommandLine, you should suppress or disable this warning. + +### OCL0017 + +Methods that are not public and static will be ignored. + +If the `CommandLineArgumentAttribute` is used on a method that is not a `public static` method, no +argument will be generated for this method. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // WARNING: the method must be public + [CommandLineAttribute] + private static void Argument(string value, int value2); +} +``` + +### OCL0018 + +Properties that are not public instance properties will be ignored. + +If the `CommandLineArgumentAttribute` is used on a property that is not a `public` property, or +that is a `static` property, no argument will be generated for this property. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // WARNING: the property must be public + [CommandLineAttribute] + private string? Argument { get; set; } +} +``` + +### OCL0019 + +A command line arguments class has the `CommandAttribute` but does not implement the `ICommand` +interface. + +Without the interface, the `CommandAttribute` is ignored and the class will not be treated as a +command by a regular or generated `CommandManager`. Both the `CommandAttribute` and the `ICommand` +interface are required for commands. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +[Command] +partial class MyCommand // WARNING: The class doesn't implement ICommand +{ + [CommandLineAttribute] + public string? Argument { get; set; } +} +``` + +The inverse, implementing `ICommand` without using the `CommandAttribute`, does not generate a +warning as this is a common pattern for subcommand base classes. + +### OCL0020 + +An argument that is required, multi-value, or a method argument, specifies a default value. The +default value will not be used for these kinds of arguments. + +For a required argument, the default value is not used because not specifying an explicit value is +an error. For a multi-value argument, the default value is never used and the collection will still +be empty or null if no explicit value was given. The method for a method argument is only invoked +if the argument is explicitly provided; it is never invoked with a default value. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // WARNING: Default value is unused on a required argument. + [CommandLineAttribute(DefaultValue = "foo")] + public required string Argument { get; set; } +} +``` + +### OCL0021 + +The `CommandLineArgumentAttribute.IsRequired` property is ignored for a property with the +`required` keyword. If the `required` keyword is present, the argument is required, even if you +set the `IsRequired` property to false explicitly. + +> The `required` keyword is only available in .Net 7.0 and later; the `IsRequired` property should +> be used to create required arguments in older versions of .Net. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // WARNING: the argument will be required regardless of the value of IsRequired. + [CommandLineAttribute(IsRequired = false)] + public required string Argument { get; set; } +} +``` + +### OCL0022 + +The same position value is used for two or more arguments. + +While the actual position values do not matter--merely the order of the values do, so skipping +numbers is fine--using the same number more than once can lead to unpredictable or unstable ordering +of the arguments, which should be avoided. + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute(Position = 0)] + public string? Argument1 { get; set; } + + // WARNING: Argument2 has the same position as Argument1. + [CommandLineAttribute(Position = 0)] + public string? Argument2 { get; set; } +} +``` + +### OCL0023 + +The `ShortAliasAttribute` is ignored on an argument that does not have a short name. Set the +`CommandLineArgumentAttribute.IsShort` property to true set an explicit short name using the +`CommandLineArgumentAttribute.ShortName` property. Without a short name, any short aliases will not +be used. + +Note that the `ShortAliasAttribute` is also ignored if `ParsingMode.LongShort` is not used, which is +not checked by the source generator, because it can be changed at runtime using the +`ParseOptions.Mode` property. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +[ParseOptions(Mode = ParsingMode.LongShort)] +partial class Arguments +{ + // WARNING: The short alias is not used since the argument has no short name. + [CommandLineAttribute] + [ShortAlias('a')] + public string? Argument { get; set; } +} +``` + +### OLC0024 + +The `AliasAttribute` is ignored on an argument with no long name. An argument has no long name only +if the `CommandLineArgumentAttribute.IsLong` property is set to false. + +Note that the `AliasAttribute` may still be used if `ParsingMode.LongShort` is not used, which is +not checked by the source generator, because it can be changed at runtime using the +`ParseOptions.Mode` property. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +[ParseOptions(Mode = ParsingMode.LongShort)] +partial class Arguments +{ + // WARNING: The long alias is not used since the argument has no long name. + [CommandLineAttribute(IsLong = false, IsShort = true)] + [Alias("arg")] + public string? Argument { get; set; } +} +``` + +### OCL0025 + +The `CommandLineArgumentAttribute.IsHidden` property is ignored for positional arguments. + +Positional arguments cannot be hidden, because excluding them from the usage help would give +incorrect positions for any additional positional arguments. A positional argument is therefore not +hidden even if `IsHidden` is set to true. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // WARNING: The argument is not hidden because it's positional. + [CommandLineAttribute(Position = 0, IsHidden = true)] + public string? Argument { get; set; } +} +``` + +### OCL0026 + +The namespace specified in the `GeneratedConverterNamespaceAttribute` is not a valid C# namespace +name, for example because one of the elements contains an unsupported character or starts with a +digit. + +For example, the following code triggers this warning: + +```csharp +[assembly: GeneratedConverterNamespace("MyApp.5Invalid")] +``` + +### OCL0027 + +The `KeyConverterAttribute`, `ValueConverterAttribute`, `KeyValueSeparatorAttribute` and +`AllowDuplicateDictionaryKeysAttribute` attributes are only used for dictionary arguments, and will +be ignored if the argument is not a dictionary argument. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute] + [AllowDuplicateDictionaryKeys] // WARNING: Ignored on non-dictionary arguments + public string? Argument { get; set; } +} +``` + +### OCL0028 + +The `KeyConverterAttribute`, `ValueConverterAttribute`, and `KeyValueSeparatorAttribute` attributes +are used by the default `KeyValuePairConverter` for dictionary arguments, and will be ignored if the +argument uses the `ArgumentConverterAttribute` to specify a different converter. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute] + [ArgumentConverter(typeof(CustomKeyValuePairConverter))] + [KeyValueSeparator(":")] // WARNING: Ignored on dictionary arguments with an explicit converter. + public Dictionary? Argument { get; set; } +} +``` + +### OCL0029 + +The `MultiValueSeparatorAttribute` is only used for multi-value arguments (including dictionary +arguments), and will be ignored if the argument is not a multi-value argument. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute] + [MultiValueSeparator(",")] // WARNING: Ignored on non-multi-value arguments + public string? Argument { get; set; } +} +``` + +### OCL0030 + +An argument has an explicit name or short name starting with a number, which cannot be used with +the '-' prefix. + +If the `CommandLineParser` sees a dash followed by a digit, it will always interpret this as a +value, because it may be a negative number. It is never interpreted as an argument name, even if +the rest of the argument is not a valid number. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +partial class Arguments +{ + // WARNING: Name starts with a number. + [CommandLineAttribute("1Arg")] + public string? Argument { get; set; } +} +``` + +This warning may be a false positive if you are using a different argument name prefix with the +`ParseOptionAttribute.ArgumentNamePrefixes` or `ParseOptions.ArgumentNamePrefixes` property, or if +you are using long/short mode and the name is a long name. In these cases, you should suppress or +disable this warning. From 1e94f5a455bd6fe9c1f63b4c3e3b8b68c9825c41 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 26 May 2023 14:23:16 -0700 Subject: [PATCH 083/234] Diagnostics for long/short name usage. --- .../CommandLineArgumentAttributeInfo.cs | 3 ++ .../ConverterGenerator.cs | 3 +- .../Diagnostics.cs | 16 +++++++ .../ParserGenerator.cs | 44 +++++++++++++------ .../Properties/Resources.Designer.cs | 36 +++++++++++++++ .../Properties/Resources.resx | 12 +++++ src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 2 +- .../NullableArgumentTypes.cs | 2 +- .../MultiValueSeparatorAttribute.cs | 2 +- src/Samples/TrimTest/Program.cs | 17 ++++--- 10 files changed, 109 insertions(+), 28 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs index fede2bce..fdbdc8f7 100644 --- a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs +++ b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs @@ -37,6 +37,7 @@ public CommandLineArgumentAttributeInfo(AttributeData data) case nameof(IsShort): _isShort = (bool)named.Value.Value!; + ExplicitIsShort = _isShort; break; case nameof(ShortName): @@ -66,6 +67,8 @@ public CommandLineArgumentAttributeInfo(AttributeData data) public bool IsShort => _isShort || ShortName != '\0'; + public bool? ExplicitIsShort { get; } + public char ShortName { get; } public bool IsLong { get; } = true; diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index beb5bf7c..b380a3ea 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -244,8 +244,7 @@ private static string GetGeneratedNamespace(TypeHelper typeHelper, SourceProduct return DefaultGeneratedNamespace; } - var ns = attribute.ConstructorArguments.FirstOrDefault().Value as string; - if (ns == null) + if (attribute.ConstructorArguments.FirstOrDefault().Value is not string ns) { return DefaultGeneratedNamespace; } diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 14d22465..b0060a82 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -283,6 +283,22 @@ public static Diagnostic ArgumentStartsWithNumber(ISymbol member, string name) = name, member.ToDisplayString()); + public static Diagnostic NoLongOrShortName(ISymbol member, AttributeData attribute) => CreateDiagnostic( + "OCL0031", + nameof(Resources.NoLongOrShortNameTitle), + nameof(Resources.NoLongOrShortNameMessageFormat), + DiagnosticSeverity.Error, + attribute.GetLocation(), + member.ToDisplayString()); + + public static Diagnostic IsShortIgnored(ISymbol member, AttributeData attribute) => CreateDiagnostic( + "OCL0032", + nameof(Resources.IsShortIgnoredTitle), + nameof(Resources.IsShortIgnoredMessageFormat), + DiagnosticSeverity.Warning, + attribute.GetLocation(), + member.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 194141f0..75249a98 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -164,7 +164,10 @@ private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isComman { foreach (var member in current.GetMembers()) { - GenerateArgument(member, ref requiredProperties); + if (!GenerateArgument(member, ref requiredProperties)) + { + return false; + } } current = current.BaseType; @@ -219,23 +222,29 @@ private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isComman return true; } - private void GenerateArgument(ISymbol member, ref List<(string, string, string)>? requiredProperties) + 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; + return true; } var attributes = new ArgumentAttributes(member.GetAttributes(), _typeHelper, _context); - // Check if it is an attribute. + // Check if it is an argument. if (attributes.CommandLineArgument == null) { - return; + 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; @@ -244,7 +253,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> if (property.DeclaredAccessibility != Accessibility.Public || property.IsStatic) { _context.ReportDiagnostic(Diagnostics.NonPublicInstanceProperty(property)); - return; + return true; } originalArgumentType = property.Type; @@ -254,14 +263,14 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> if (method.DeclaredAccessibility != Accessibility.Public || !method.IsStatic) { _context.ReportDiagnostic(Diagnostics.NonPublicStaticMethod(method)); - return; + return true; } methodInfo = DetermineMethodArgumentInfo(method); if (methodInfo is not MethodArgumentInfo methodInfoValue) { _context.ReportDiagnostic(Diagnostics.InvalidMethodSignature(method)); - return; + return false; } originalArgumentType = methodInfoValue.ArgumentType; @@ -269,7 +278,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> else { // How did we get here? Already checked above. - return; + return true; } var argumentType = originalArgumentType.WithNullableAnnotation(NullableAnnotation.NotAnnotated); @@ -304,7 +313,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> var multiValueType = DetermineMultiValueType(property, argumentType); if (multiValueType is not var (collectionType, dictionaryType, multiValueElementType)) { - return; + return false; } if (dictionaryType != null) @@ -326,14 +335,14 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> if (keyConverter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); - return; + return false; } var valueConverter = DetermineConverter(member, valueType.GetUnderlyingType(), attributes.ValueConverter, valueType.IsNullableValueType()); if (valueConverter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, keyType.GetUnderlyingType())); - return; + return false; } var separator = attributes.KeyValueSeparator == null @@ -356,7 +365,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> if (property.SetMethod != null && property.SetMethod.IsInitOnly && !property.IsRequired) { _context.ReportDiagnostic(Diagnostics.NonRequiredInitOnlyProperty(property)); - return; + return false; } if (property.IsRequired) @@ -376,7 +385,7 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> if (converter == null) { _context.ReportDiagnostic(Diagnostics.NoConverter(member, elementType)); - return; + return false; } // The leading commas are not a formatting I like but it does make things easier here. @@ -558,6 +567,13 @@ private void GenerateArgument(ISymbol member, ref List<(string, string, string)> { _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) diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 46388d55..0ade5774 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -420,6 +420,24 @@ internal static string IsRequiredWithRequiredPropertyTitle { } } + /// + /// 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 No command line argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter.. /// @@ -438,6 +456,24 @@ internal static string NoConverterTitle { } } + /// + /// 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}.{1} will not create a command line argument because it is not a public instance property.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 1067a59f..3fddd9b1 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -237,12 +237,24 @@ 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. + No command line argument converter exists for type {0} used by the argument defined by {1}.{2}, 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}.{1} will not create a command line argument because it is not a public instance property. diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 406c7ba1..267d1eb6 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -9,7 +9,7 @@ using System.Net; // We deliberately have some properties and methods that cause warnings, so disable those. -#pragma warning disable CLW0002,CLW0003,CLW0005,CLW0008,CLW0009,CLW0013 +#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029 namespace Ookii.CommandLine.Tests; diff --git a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs index b63bd82b..c6219678 100644 --- a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs @@ -8,7 +8,7 @@ namespace Ookii.CommandLine.Tests; // We deliberately have some properties and methods that cause warnings, so disable those. -#pragma warning disable CLW0006 +#pragma warning disable OCL0021 class NullReturningStringConverter : ArgumentConverter { diff --git a/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs b/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs index f0621edb..248b6398 100644 --- a/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs +++ b/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs @@ -26,7 +26,7 @@ namespace Ookii.CommandLine /// /// 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. + /// positional arguments until another named argument has been supplied. /// /// /// Using white-space separators will not work if the diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 18248e81..328b9bbb 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -8,25 +8,24 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -var manager = new TestManager(); -return manager.RunCommand() ?? 1; +//var manager = new TestManager(); +//return manager.RunCommand() ?? 1; -//var arguments = Arguments.Parse(); -//if (arguments != null) -//{ -// Console.WriteLine($"Hello, World! {arguments.Test}"); -//} +var arguments = Arguments.Parse(); +if (arguments != null) +{ + Console.WriteLine($"Hello, World! {arguments.Test}"); +} [GeneratedCommandManager] partial class TestManager { } [GeneratedParser] -[ParseOptions(CaseSensitive = true)] +[ParseOptions(CaseSensitive = true, Mode = ParsingMode.LongShort)] [Description("This is a test")] [ApplicationFriendlyName("Trim Test")] [RequiresAny(nameof(Test), nameof(Test2))] -[Command] partial class Arguments : ICommand { [CommandLineArgument(Position = 0)] From 1fbb2e967008c57942ff5f148dfc8b528359b085 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 26 May 2023 14:44:03 -0700 Subject: [PATCH 084/234] Documentation for new diagnostics. --- docs/SourceGenerationDiagnostics.md | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index fb1ee045..16da1b74 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -335,6 +335,30 @@ partial class Arguments To fix this error, either use the constructor that takes a `Type` using the `typeof` keyword, or use a `CommandLineParser` without using source generation. +### OCL0031 + +The argument does not have a long name or a short name. This happens when both the +`CommandLineArgumentAttribute.IsLong` and `CommandLineArgumentAttribute.IsShort` properties are set +to false. This means that when using [long/short mode](Arguments.md#longshort-mode), the argument +would not be usable. + +This error will be triggered regardless of the parsing mode you actually use, since that can be +changed at runtime by the `ParseOptions.Mode` property and is therefore not known at compile time. + +For example, the following code triggers this error: + +```csharp +[GeneratedParser] +[ParseOptions(Mode = ParsingMode.LongShort)] +partial class Arguments +{ + // ERROR: No long or short name (IsShort is false by default). + [CommandLineAttribute(IsLong = false)] + [ArgumentConverter("MyNamespace.MyConverter")] + public CustomType? Argument { get; set; } +} +``` + ## Warnings ### OCL0016 @@ -646,3 +670,24 @@ This warning may be a false positive if you are using a different argument name `ParseOptionAttribute.ArgumentNamePrefixes` or `ParseOptions.ArgumentNamePrefixes` property, or if you are using long/short mode and the name is a long name. In these cases, you should suppress or disable this warning. + +### OCL0032 + +The `CommandLineArgumentAttribute.IsShort` property is ignored if an explicit short name is set +using the `CommandLineArgumentAttribute.ShortName` property. + +If the `ShortName` property is set, it implies that `IsShort` is true, and manually setting it to +false will have no effect. + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +[ParseOptions(Mode = ParsingMode.LongShort)] +partial class Arguments +{ + // WARNING: Argument has a short name so IsShort is ignored. + [CommandLineAttribute(ShortName = 'a', IsShort = false)] + public string? Argument { get; set; } +} +``` From 8b0b9186de5318acf817c36dc13cf14ba5c1e004 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 26 May 2023 14:44:56 -0700 Subject: [PATCH 085/234] Fix typo. --- docs/SourceGenerationDiagnostics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 16da1b74..0844fd62 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -15,7 +15,7 @@ Without source generation, these mistakes would either lead to a runtime excepti catch any problems during compile time, which reduces the risk of bugs. Not all errors can be caught at compile time. For example, the source generator does not check for -duplicate argument names, because the `ParseOptions.ArgumentNameTransform` property and +duplicate argument names, because the `ParseOptions.ArgumentNameTransform` and `ParseOptions.ArgumentNameComparison` properties can render the result of this check inaccurate. ## Errors From 5c2262bcc010b1b3f24266691975933aea39e834 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 26 May 2023 15:03:02 -0700 Subject: [PATCH 086/234] Updated migration docs. --- README.md | 2 +- docs/Migrating.md | 27 ++++++++++++++++++++++++--- docs/README.md | 2 +- license.md | 2 +- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b04a678a..cfc66b8e 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ coding style best. 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/Migrating.md b/docs/Migrating.md index 3235eab7..74da633f 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -1,6 +1,6 @@ -# 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 @@ -12,7 +12,28 @@ 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 + +- 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 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`. + - This change enables more flexibility, better performance by supporting conversions using + `ReadOnlySpan`, and enables trimming your assembly when combined with + [source generation](SourceGeneration.md). +- Constructor parameters can no longer be used to define command line arguments. Instead, all + arguments must be defined using properties. If you were using constructor parameters to avoid + setting a default value for a non-nullable reference type, you can use the `required` keyword + instead if using .Net 7.0 or later. +- 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. +- The `CommandInfo` type is now a class instead of a structure. + +## 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. diff --git a/docs/README.md b/docs/README.md index 5511bfbc..c95fca59 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ explanations of what they do and examples of their output. ## Contents - [What's new in Ookii.CommandLine](ChangeLog.md) -- [Migrating from Ookii.CommandLine 2.x](Migrating.md) +- [Migrating from Ookii.CommandLine 2.x / 3.x](Migrating.md) - [Tutorial: getting started with Ookii.CommandLine](Tutorial.md) - [Command line arguments in Ookii.CommandLine](Arguments.md) - [Defining command line arguments](DefiningArguments.md) diff --git a/license.md b/license.md index 93989f71..59203cfa 100644 --- a/license.md +++ b/license.md @@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. From 92740bedf929772563180e8aa39ef609be3fd841 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 26 May 2023 15:15:42 -0700 Subject: [PATCH 087/234] Added docs about property initializer default value. --- docs/SourceGeneration.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/SourceGeneration.md b/docs/SourceGeneration.md index a45e0406..0094d024 100644 --- a/docs/SourceGeneration.md +++ b/docs/SourceGeneration.md @@ -18,6 +18,7 @@ Using source generation has several benefits: [trimmed](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options). When source generation is not used, the way Ookii.CommandLine uses reflection prevents trimming entirely. +- Specify [default values using property initializers](#default-values-using-property-initializers). - Improved performance. Benchmarks show that instantiating a `CommandLineParser` using a generated parser is up to thirty times faster than using reflection. However, since we're still talking about microseconds, this is unlikely to matter that much to a typical application. @@ -120,6 +121,40 @@ Generating the `Parse()` methods is optional, and can be disabled using the `GeneratedParserAttribute.GenerateParseMethods` property. The `CreateParser()` method is always generated. +### Default values using property initializers + +When using the source generation to create a command line parser, you can use property initializers +to specify the default value of an argument, and still have that value be used in the usage help. + +```csharp +[GeneratedParser] +partial class Arguments +{ + [CommandLineArgument(DefaultValue = "foo")] + public string? Arg1 { get; set; } + + [CommandLineArgument] + public string Arg2 { get; set; } = "foo"; +} +``` + +When using the reflection-based parser with the default constructors of the `CommandLineParser` +class, `Arg2` would have its value set to "foo" when omitted (since Ookii.CommandLine doesn't +assign the property if the argument is not specifies), but that default value would not be included +in the usage help, whereas `Arg1` does. + +With source generation, both `Arg1` and `Arg2` will have the default value of "foo" shown in the +usage help, making the two forms identical. Additionally, `Arg2` could be marked non-nullable +because it was initialized to a non-null value, something which isn't possible for `Arg1` without +initializing the property to a value that will not be used. + +If both a property initializer and the `DefaultValue` property are both used, the `DefaultValue` +property takes precedence. + +Note that this only works if the property initializer is a literal. If a different kind of value is +used in the property initialized, such as a reference to a constant or a function call, the value +will not be shown in the usage help. + ## Generating a command manager Just like the `CommandLineParser` class, the `CommandManager` class normally uses reflection to From 94a4fd251fed96a495e805c50aff5ec6db749b2e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 26 May 2023 15:26:01 -0700 Subject: [PATCH 088/234] Fixed: ParseOptions.AutoVersionArgument default value would ignore ParseOptionsAttribute. --- src/Ookii.CommandLine/ParseOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 2b23bfc8..932fa952 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -319,7 +319,7 @@ public class ParseOptions /// /// /// - public bool? AutoVersionArgument { get; set; } = true; + public bool? AutoVersionArgument { get; set; } /// /// Gets or sets the color applied to error messages. From 18702fcee5534fdf371a0219059989f7c5a2c731 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 26 May 2023 16:11:51 -0700 Subject: [PATCH 089/234] Automatic prefix aliases. --- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 14 + .../CommandLineParserTest.cs | 30 + src/Ookii.CommandLine/CommandLineParser.cs | 49 +- src/Ookii.CommandLine/ParseOptions.cs | 37 ++ .../ParseOptionsAttribute.cs | 618 +++++++++--------- 5 files changed, 454 insertions(+), 294 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 267d1eb6..3b51f032 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -605,3 +605,17 @@ partial class InitializerDefaultValueArguments [CommandLineArgument] public int Arg3 { get; set; } = int.MaxValue; } + +[GeneratedParser] +partial class AutoPrefixAliasesArguments +{ + [CommandLineArgument(IsShort = true)] + public string Protocol { get; set; } + + [CommandLineArgument] + public int Port { get; set; } + + [CommandLineArgument(IsShort = true)] + [Alias("Prefix")] + public bool EnablePrefix { get; set; } +} diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 501d4444..9df2c3f8 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1154,6 +1154,36 @@ public void TestInitializerDefaultValues() Assert.IsNull(parser.GetArgument("Arg3").DefaultValue); } + [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.Parse(new[] { "-p", "foo" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "p"); + + // Ambiguous due to alias. + CheckThrows(() => parser.Parse(new[] { "-pr", "foo" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "pr"); + + // 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.Parse(new[] { "-pro", "foo", "-Po", "5", "-e" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "pro"); + } + private class ExpectedArgument { public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind.SingleValue) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 5628b9a2..ce306ae9 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1622,7 +1622,15 @@ private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) if (argument == null && !_argumentsByName.TryGetValue(argumentName, out argument)) { - throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, argumentName.ToString()); + if (Options.AutoPrefixAliases ?? true) + { + argument = GetArgumentByNamePrefix(argumentName.Span); + } + + if (argument == null) + { + throw StringProvider.CreateException(CommandLineArgumentErrorCategory.UnknownArgument, argumentName.ToString()); + } } argument.SetUsedArgumentName(argumentName); @@ -1662,6 +1670,45 @@ private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) return ParseArgumentValue(argument, null, argumentValue) ? -1 : index; } + 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) + { + continue; + } + + var matches = argument.ArgumentName.AsSpan().StartsWith(prefix, ArgumentNameComparison); + if (!matches && argument.Aliases != null) + { + foreach (var alias in argument.Aliases) + { + if (alias.AsSpan().StartsWith(prefix, ArgumentNameComparison)) + { + matches = true; + break; + } + } + } + + if (matches) + { + if (foundArgument != null) + { + // Prefix is not unique. + return null; + } + + foundArgument = argument; + } + } + + return foundArgument; + } + private bool ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? value) { foreach (var ch in name) diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 932fa952..838d55e5 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -321,6 +321,42 @@ public class ParseOptions /// public bool? AutoVersionArgument { get; set; } + /// + /// 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 "Port" 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 or sets the color applied to error messages. /// @@ -561,6 +597,7 @@ public void Merge(ParseOptionsAttribute attribute) NameValueSeparator ??= attribute.NameValueSeparator; AutoHelpArgument ??= attribute.AutoHelpArgument; AutoVersionArgument ??= attribute.AutoVersionArgument; + AutoPrefixAliases ??= attribute.AutoPrefixAliases; ValueDescriptionTransform ??= attribute.ValueDescriptionTransform; } diff --git a/src/Ookii.CommandLine/ParseOptionsAttribute.cs b/src/Ookii.CommandLine/ParseOptionsAttribute.cs index a373951c..793a3f07 100644 --- a/src/Ookii.CommandLine/ParseOptionsAttribute.cs +++ b/src/Ookii.CommandLine/ParseOptionsAttribute.cs @@ -1,323 +1,355 @@ 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 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 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. +/// +/// +/// If you also use the class, any options provided there will +/// override the options set in this attribute. +/// +/// +/// If you wish to use the default options, you do not need to apply this attribute to your +/// class at all. +/// +/// +[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 how names are created for arguments that don't have + /// an explicit name. + /// + /// + /// One of the values of the enumeration. The default value is + /// . + /// + /// /// - /// 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. + /// 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. /// /// - /// If you also use the class, any options provided there will - /// override the options set in this attribute. + /// The name transformation will also be applied to the names of the automatically added + /// help and version attributes. /// /// - /// If you wish to use the default options, you do not need to apply this attribute to your - /// class at all. + /// This value can be overridden by the + /// property. /// /// - [AttributeUsage(AttributeTargets.Class)] - public class ParseOptionsAttribute : Attribute - { - /// - /// 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; } + 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 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 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 . - /// - /// - /// 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 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 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 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 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 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 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 . - /// - /// - /// 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 . - /// - /// - /// 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 "Port" 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. + /// + /// + /// 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 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 + /// property or the + /// property. + /// + /// + /// This value can be overridden by the + /// property. + /// + /// + /// + public NameTransform ValueDescriptionTransform { get; set; } - internal StringComparison GetStringComparison() + internal StringComparison GetStringComparison() + { + if (CaseSensitive) + { + // Do not use Ordinal for case-sensitive comparisons so that when sorting capitals + // and non-capitals are sorted together. + return StringComparison.InvariantCulture; + } + else { - if (CaseSensitive) - { - // 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; - } + return StringComparison.OrdinalIgnoreCase; } } } From b7af76823a2f42816fce967e2acbc78268bea14d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 30 May 2023 14:37:58 -0700 Subject: [PATCH 090/234] Emit a warning for missing argument descriptions. --- docs/SourceGenerationDiagnostics.md | 38 +++++++++++++++++++ .../Diagnostics.cs | 8 ++++ .../ParserGenerator.cs | 17 ++++++++- .../Properties/Resources.Designer.cs | 18 +++++++++ .../Properties/Resources.resx | 6 +++ src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 2 +- src/Ookii.CommandLine.Tests/CommandTypes.cs | 2 + .../NullableArgumentTypes.cs | 2 +- src/Samples/TrimTest/Program.cs | 2 + 9 files changed, 91 insertions(+), 4 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 0844fd62..0e3e7223 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -691,3 +691,41 @@ partial class Arguments public string? Argument { get; set; } } ``` + +### OCL0033 + +Arguments should have a description, set using the `DescriptionAttribute` attribute, for use in the +usage help. + +Arguments without a description are not guaranteed to be listed in the description list of the +usage help, and provide no additional information about their use when the user requests usage +help (for example using the automatic `-Help` argument). + +For example, the following code triggers this warning: + +```csharp +[GeneratedParser] +partial class Arguments +{ + /// WARNING: No DescriptionAttribute on this member. + [CommandLineAttribute] + public string? Argument { get; set; } +} +``` + +To fix this, write a concise description explaining the argument's purpose and usage, and apply the +`DescriptionAttribute` (or a derived attribute) to the member that defines the argument. + +```csharp +[GeneratedParser] +partial class Arguments +{ + /// WARNING: No DescriptionAttribute on this member. + [CommandLineAttribute] + [Description("A description of the argument.")] + public string? Argument { get; set; } +} +``` + +This warning will not be emitted for arguments that are hidden using the +`CommandLineArgumentAttribute.IsHidden` property. diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index b0060a82..d69c1d25 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -299,6 +299,14 @@ public static Diagnostic IsShortIgnored(ISymbol member, AttributeData attribute) attribute.GetLocation(), member.ToDisplayString()); + public static Diagnostic ArgumentWithoutDescription(ISymbol member) => CreateDiagnostic( + "OCL0033", + nameof(Resources.ArgumentWithoutDescriptionTitle), + nameof(Resources.ArgumentWithoutDescriptionMessageFormat), + DiagnosticSeverity.Warning, + member.Locations.FirstOrDefault(), + member.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 75249a98..d6e29a4b 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -550,9 +550,22 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> _context.ReportDiagnostic(Diagnostics.AliasWithoutLongName(attributes.Aliases.First(), member)); } - if (argumentInfo.IsHidden && argumentInfo.Position != null) + bool isHidden = false; + if (argumentInfo.IsHidden) { - _context.ReportDiagnostic(Diagnostics.IsHiddenWithPositional(member)); + if (argumentInfo.Position != null) + { + _context.ReportDiagnostic(Diagnostics.IsHiddenWithPositional(member)); + } + else + { + isHidden = true; + } + } + + if (!isHidden && attributes.Description == null) + { + _context.ReportDiagnostic(Diagnostics.ArgumentWithoutDescription(member)); } CheckIgnoredDictionaryAttribute(member, isDictionary, attributes.Converter, attributes.KeyConverter); diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 0ade5774..9bcdb47d 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -114,6 +114,24 @@ internal static string ArgumentStartsWithNumberTitle { } } + /// + /// 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.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 3fddd9b1..2f8e17bc 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -135,6 +135,12 @@ 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. diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 3b51f032..f8637a40 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -9,7 +9,7 @@ using System.Net; // We deliberately have some properties and methods that cause warnings, so disable those. -#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029 +#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029,OCL0033 namespace Ookii.CommandLine.Tests; diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index d3edcbee..ce6b2a4a 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -3,6 +3,8 @@ using System.ComponentModel; using System.Threading.Tasks; +#pragma warning disable OCL0033 + namespace Ookii.CommandLine.Tests; [GeneratedCommandManager] diff --git a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs index c6219678..15b54850 100644 --- a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs @@ -8,7 +8,7 @@ namespace Ookii.CommandLine.Tests; // We deliberately have some properties and methods that cause warnings, so disable those. -#pragma warning disable OCL0021 +#pragma warning disable OCL0021,OCL0033 class NullReturningStringConverter : ArgumentConverter { diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs index 328b9bbb..546fa620 100644 --- a/src/Samples/TrimTest/Program.cs +++ b/src/Samples/TrimTest/Program.cs @@ -8,6 +8,8 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +#pragma warning disable OCL0033 + //var manager = new TestManager(); //return manager.RunCommand() ?? 1; From 675ffc046f221375a95e5d7738e4f2192c3eb255 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 30 May 2023 14:49:57 -0700 Subject: [PATCH 091/234] Emit a warning for commands without a description. --- docs/SourceGenerationDiagnostics.md | 38 ++++++++++++++++++- .../CommandAttributeInfo.cs | 21 ++++++++++ .../Diagnostics.cs | 8 ++++ .../ParserGenerator.cs | 9 +++++ .../Properties/Resources.Designer.cs | 27 ++++++++----- .../Properties/Resources.resx | 9 +++-- src/Ookii.CommandLine.Tests/CommandTypes.cs | 3 +- 7 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 src/Ookii.CommandLine.Generator/CommandAttributeInfo.cs diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 0e3e7223..e97d72d5 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -720,7 +720,6 @@ To fix this, write a concise description explaining the argument's purpose and u [GeneratedParser] partial class Arguments { - /// WARNING: No DescriptionAttribute on this member. [CommandLineAttribute] [Description("A description of the argument.")] public string? Argument { get; set; } @@ -729,3 +728,40 @@ partial class Arguments This warning will not be emitted for arguments that are hidden using the `CommandLineArgumentAttribute.IsHidden` property. + +### OCL0034 + +Subcommands should have a description, set using the `DescriptionAttribute` attribute, for use in +the usage help. + +For example, the following code triggers this warning: + +```csharp +/// WARNING: No DescriptionAttribute on this subcommand class. +[GeneratedParser] +[Command] +partial class MyCommand : ICommand +{ + [CommandLineAttribute] + [Description("A description of the argument.")] + public string? Argument { get; set; } +} +``` + +To fix this, write a concise description explaining the command's purpose, and apply the +`DescriptionAttribute` (or a derived attribute) to the class that defines the command. + +```csharp +[GeneratedParser] +[Description("A description of the command.")] +[Command] +partial class MyCommand : ICommand +{ + [CommandLineAttribute] + [Description("A description of the argument.")] + public string? Argument { get; set; } +} +``` + +This warning will not be emitted for subcommands that are hidden using the +`CommandAttribute.IsHidden` property. diff --git a/src/Ookii.CommandLine.Generator/CommandAttributeInfo.cs b/src/Ookii.CommandLine.Generator/CommandAttributeInfo.cs new file mode 100644 index 00000000..8cba981e --- /dev/null +++ b/src/Ookii.CommandLine.Generator/CommandAttributeInfo.cs @@ -0,0 +1,21 @@ +using Microsoft.CodeAnalysis; + +namespace Ookii.CommandLine.Generator; + +internal class CommandAttributeInfo +{ + public CommandAttributeInfo(AttributeData data) + { + foreach (var named in data.NamedArguments) + { + switch (named.Key) + { + case nameof(IsHidden): + IsHidden = (bool)named.Value.Value!; + break; + } + } + } + + public bool IsHidden { get; } +} diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index d69c1d25..aff2d6e7 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -307,6 +307,14 @@ public static Diagnostic ArgumentWithoutDescription(ISymbol member) => CreateDia member.Locations.FirstOrDefault(), member.ToDisplayString()); + public static Diagnostic CommandWithoutDescription(ISymbol symbol) => CreateDiagnostic( + "OCL0034", + nameof(Resources.CommandWithoutDescriptionTitle), + nameof(Resources.CommandWithoutDescriptionMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index d6e29a4b..e3b558ca 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -117,6 +117,15 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen return null; } + if (isCommand && attributes.Description == null) + { + var commandInfo = new CommandAttributeInfo(attributes.Command!); + if (!commandInfo.IsHidden) + { + _context.ReportDiagnostic(Diagnostics.CommandWithoutDescription(_argumentsClass)); + } + } + _builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{_argumentsClass.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new GeneratedProvider(), options);"); _builder.AppendLine(); var nullableType = _argumentsClass.WithNullableAnnotation(NullableAnnotation.Annotated); diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 9bcdb47d..a311e936 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -204,6 +204,24 @@ internal static string CommandAttributeWithoutInterfaceTitle { } } + /// + /// 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.. /// @@ -618,15 +636,6 @@ internal static string ShortAliasWithoutShortNameTitle { } } - /// - /// Looks up a localized string similar to . - /// - internal static string String1 { - get { - return ResourceManager.GetString("String1", resourceCulture); - } - } - /// /// Looks up a localized string similar to The type {0} must be a reference type (class) when the {1} attribute is used.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 2f8e17bc..5ea5eb25 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -165,6 +165,12 @@ The command line arguments class has the CommandAttribute but does not implement ICommand. + + 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. @@ -303,9 +309,6 @@ 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. diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index ce6b2a4a..5b3955c8 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -3,7 +3,7 @@ using System.ComponentModel; using System.Threading.Tasks; -#pragma warning disable OCL0033 +#pragma warning disable OCL0033,OCL0034 namespace Ookii.CommandLine.Tests; @@ -17,7 +17,6 @@ partial class GeneratedManagerWithExplicitAssembly { } [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.")] From 64c741791b96850d5711c12e5a65f78182d18a26 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 30 May 2023 15:45:54 -0700 Subject: [PATCH 092/234] Use ReadOnlySpan for arguments. --- src/Ookii.CommandLine.Tests/CommandTypes.cs | 4 +- src/Ookii.CommandLine/CommandLineParser.cs | 79 +++++++++++++------ src/Ookii.CommandLine/Commands/CommandInfo.cs | 25 +++++- .../Commands/CommandManager.cs | 63 +++++++++------ .../Commands/ICommandWithCustomParsing.cs | 9 ++- src/Samples/NestedCommands/ParentCommand.cs | 11 ++- 6 files changed, 127 insertions(+), 64 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index 5b3955c8..f70b399a 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -50,9 +50,9 @@ public int Run() [Description("Custom parsing command.")] partial class CustomParsingCommand : ICommandWithCustomParsing { - public void Parse(string[] args, int index, CommandOptions options) + public void Parse(ReadOnlyMemory args, CommandManager manager) { - Value = args[index]; + Value = args.Span[0]; } public string Value { get; set; } diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index ce306ae9..9b0a9aef 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -898,22 +898,15 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// - /// Parses the specified command line arguments, starting at the specified index. + /// Parses the specified command line arguments. /// /// 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) + public object? Parse(ReadOnlySpan args) { try { HelpRequested = false; - return ParseCore(args, index); + return ParseCore(args); } catch (CommandLineArgumentException ex) { @@ -923,6 +916,33 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = } } + /// + /// + /// 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) + { + if (args == null) + { + throw new ArgumentNullException(nameof(index)); + } + + if (index < 0 || index > args.Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + return Parse(args.AsSpan(index)); + } + /// /// Parses the arguments returned by the /// method, and displays error messages and usage help if required. @@ -969,6 +989,27 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// does not fall within the bounds of . /// public object? ParseWithErrorHandling(string[] args, int index = 0) + { + if (args == null) + { + throw new ArgumentNullException(nameof(index)); + } + + if (index < 0 || index > args.Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + return ParseWithErrorHandling(args.AsSpan(index)); + } + + /// + /// + /// Parses the specified command line arguments, and displays error messages and usage help if + /// required. + /// + /// The command line arguments. + public object? ParseWithErrorHandling(ReadOnlySpan args) { EventHandler? handler = null; if (_parseOptions.DuplicateArguments == ErrorMode.Warning) @@ -986,7 +1027,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = object? result = null; try { - result = Parse(args, index); + result = Parse(args); } catch (CommandLineArgumentException ex) { @@ -1452,18 +1493,8 @@ private void VerifyPositionalArgumentRules() } } - private object? ParseCore(string[] args, int index) + private object? ParseCore(ReadOnlySpan args) { - if (args == null) - { - throw new ArgumentNullException(nameof(index)); - } - - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - // Reset all arguments to their default value. foreach (CommandLineArgument argument in _arguments) { @@ -1473,7 +1504,7 @@ private void VerifyPositionalArgumentRules() HelpRequested = false; int positionalArgumentIndex = 0; - for (int x = index; x < args.Length; ++x) + for (int x = 0; x < args.Length; ++x) { string arg = args[x]; var argumentNamePrefix = CheckArgumentNamePrefix(arg); @@ -1601,7 +1632,7 @@ private bool ParseArgumentValue(CommandLineArgument argument, string? stringValu return cancel; } - private int ParseNamedArgument(string[] args, int index, PrefixInfo prefix) + private int ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo prefix) { var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitOnce(NameValueSeparator); diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index 007559de..7825c857 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -175,7 +175,7 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) { if (args == null) { - throw new ArgumentNullException(nameof(args)); + throw new ArgumentNullException(nameof(index)); } if (index < 0 || index > args.Length) @@ -183,16 +183,35 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) throw new ArgumentOutOfRangeException(nameof(index)); } + return CreateInstanceWithResult(args.AsMemory(index)); + } + + /// + /// Creates an instance of the command type. + /// + /// 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. + /// + /// + /// + /// The property of the returned + /// will be if the command used custom parsing. + /// + /// + public (ICommand?, ParseResult) CreateInstanceWithResult(ReadOnlyMemory args) + { if (UseCustomArgumentParsing) { var command = CreateInstanceWithCustomParsing(); - command.Parse(args, index, _manager.Options); + command.Parse(args, _manager); return (command, default); } else { var parser = CreateParser(); - var command = (ICommand?)parser.ParseWithErrorHandling(args, index); + var command = (ICommand?)parser.ParseWithErrorHandling(args.Span); return (command, parser.ParseResult); } } diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index c48ac23c..39da3a0c 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -301,17 +301,10 @@ public IEnumerable GetCommands() /// /// 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 @@ -335,18 +328,8 @@ public IEnumerable GetCommands() /// automatic version command, and not any other command name. /// /// - public ICommand? CreateCommand(string? commandName, string[] args, int index) + public ICommand? CreateCommand(string? commandName, ReadOnlyMemory args) { - if (args == null) - { - throw new ArgumentNullException(nameof(args)); - } - - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - ParseResult = default; var commandInfo = commandName == null ? null @@ -361,7 +344,7 @@ public IEnumerable GetCommands() _options.UsageWriter.CommandName = info.Name; try { - var (command, result) = info.CreateInstanceWithResult(args, index); + var (command, result) = info.CreateInstanceWithResult(args); ParseResult = result; return command; } @@ -371,6 +354,25 @@ public IEnumerable GetCommands() } } + /// + /// The name of the command. + /// The arguments to the command. + /// The index in at which to start parsing the arguments. + public ICommand? CreateCommand(string? commandName, string[] args, int index) + { + if (args == null) + { + throw new ArgumentNullException(nameof(index)); + } + + if (index < 0 || index > args.Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + return CreateCommand(commandName, args.AsMemory(index)); + } + /// /// /// Finds and instantiates the subcommand with the name from the first argument, or if that @@ -388,14 +390,25 @@ public IEnumerable GetCommands() throw new ArgumentOutOfRangeException(nameof(index)); } + return CreateCommand(args.AsMemory(index)); + } + + + /// + /// + /// Finds and instantiates the subcommand with the name from the first argument, or if that + /// fails, writes error and usage information. + /// + public ICommand? CreateCommand(ReadOnlyMemory args) + { string? commandName = null; - if (index < args.Length) + if (args.Length != 0) { - commandName = args[index]; - ++index; + commandName = args.Span[0]; + args = args.Slice(1); } - return CreateCommand(commandName, args, index); + return CreateCommand(commandName, args); } /// @@ -403,10 +416,10 @@ public IEnumerable GetCommands() /// using the first argument for the command name. If that fails, writes error and usage information. /// /// - /// + /// /// /// - /// + /// /// public ICommand? CreateCommand() { diff --git a/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs b/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs index 9df820a0..9d8cd567 100644 --- a/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs +++ b/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs @@ -1,4 +1,6 @@ -namespace Ookii.CommandLine.Commands +using System; + +namespace Ookii.CommandLine.Commands { /// /// Represents a subcommand that does its own argument parsing. @@ -19,8 +21,7 @@ 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 that was used to create this command. + void Parse(ReadOnlyMemory args, CommandManager manager); } } diff --git a/src/Samples/NestedCommands/ParentCommand.cs b/src/Samples/NestedCommands/ParentCommand.cs index 992bd20b..dd4fa732 100644 --- a/src/Samples/NestedCommands/ParentCommand.cs +++ b/src/Samples/NestedCommands/ParentCommand.cs @@ -18,22 +18,21 @@ internal abstract class ParentCommand : AsyncCommandBase, ICommandWithCustomPars { private IAsyncCommand? _childCommand; - public void Parse(string[] args, int index, CommandOptions options) + public void Parse(ReadOnlyMemory args, CommandManager manager) { // Nested commands don't need to have a "version" command. - options.AutoVersionCommand = false; + manager.Options.AutoVersionCommand = false; // Select only the commands that have a ParentCommandAttribute specifying this command // as their parent. - options.CommandFilter = + manager.Options.CommandFilter = (command) => command.CommandType.GetCustomAttribute()?.ParentCommand == GetType(); - var manager = new CommandManager(options); var info = CommandInfo.Create(GetType(), manager); // Use a custom UsageWriter to replace the application description with the // description of this command. - options.UsageWriter = new CustomUsageWriter(info) + manager.Options.UsageWriter = new CustomUsageWriter(info) { // Apply the same options as the parent command. IncludeApplicationDescriptionBeforeCommandList = true, @@ -41,7 +40,7 @@ public void Parse(string[] args, int index, CommandOptions options) }; // All commands in this sample are async, so this cast is safe. - _childCommand = (IAsyncCommand?)manager.CreateCommand(args, index); + _childCommand = (IAsyncCommand?)manager.CreateCommand(args); } public override async Task RunAsync() From afde2bdf13e3598af0b49b010dd0b47c199d0d8f Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 30 May 2023 17:48:54 -0700 Subject: [PATCH 093/234] Support for nested commands. --- .../ArgumentsClassAttributes.cs | 3 + .../CommandGenerator.cs | 17 +- .../Diagnostics.cs | 8 + .../Properties/Resources.Designer.cs | 27 + .../Properties/Resources.resx | 9 + src/Ookii.CommandLine.Generator/TypeHelper.cs | 2 + src/Ookii.CommandLine.Tests/CommandTypes.cs | 39 + .../SubCommandTest.Usage.cs | 132 +++ src/Ookii.CommandLine.Tests/SubCommandTest.cs | 865 +++++++++--------- src/Ookii.CommandLine/CommandLineParser.cs | 2 +- src/Ookii.CommandLine/Commands/CommandInfo.cs | 6 +- .../Commands/CommandManager.cs | 107 ++- .../Commands/CommandOptions.cs | 2 + .../Commands/ParentCommand.cs | 134 +++ .../Commands/ParentCommandAttribute.cs | 25 + .../Support/GeneratedCommandInfo.cs | 6 +- .../GeneratedCommandInfoWithCustomParsing.cs | 5 +- .../Support/ReflectionCommandInfo.cs | 13 +- src/Ookii.CommandLine/UsageWriter.cs | 12 +- 19 files changed, 953 insertions(+), 461 deletions(-) create mode 100644 src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs create mode 100644 src/Ookii.CommandLine/Commands/ParentCommand.cs create mode 100644 src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs diff --git a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs index ebe87edf..c5fc6c2e 100644 --- a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs +++ b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs @@ -9,6 +9,7 @@ internal readonly struct ArgumentsClassAttributes private readonly AttributeData? _applicationFriendlyName; private readonly AttributeData? _command; private readonly AttributeData? _generatedParser; + private readonly AttributeData? _parentCommand; private readonly List? _classValidators; private readonly List? _aliases; @@ -24,6 +25,7 @@ public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper, Sourc attribute.CheckType(typeHelper.ApplicationFriendlyNameAttribute, ref _applicationFriendlyName) || attribute.CheckType(typeHelper.CommandAttribute, ref _command) || attribute.CheckType(typeHelper.ClassValidationAttribute, ref _classValidators) || + attribute.CheckType(typeHelper.ParentCommandAttribute, ref _parentCommand) || attribute.CheckType(typeHelper.AliasAttribute, ref _aliases) || attribute.CheckType(typeHelper.GeneratedParserAttribute, ref _generatedParser)) { @@ -43,6 +45,7 @@ public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper, Sourc public AttributeData? ApplicationFriendlyName => _applicationFriendlyName; public AttributeData? Command => _command; public AttributeData? GeneratedParser => _generatedParser; + public AttributeData? ParentCommand => _parentCommand; public List? ClassValidators => _classValidators; public List? Aliases => _aliases; } diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index 356b7f7e..ed2eabfa 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -157,7 +157,7 @@ public void Generate() return builder.GetSource(); } - private void GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType, ArgumentsClassAttributes? commandAttributes) + private bool GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType, ArgumentsClassAttributes? commandAttributes) { var useCustomParsing = commandType.ImplementsInterface(_typeHelper.ICommandWithCustomParsing); var commandTypeName = commandType.ToDisplayString(); @@ -201,8 +201,23 @@ private void GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType } } + if (attributes.ParentCommand != null) + { + var argument = attributes.ParentCommand.ConstructorArguments[0]; + if (argument.Kind != TypedConstantKind.Type) + { + _context.ReportDiagnostic(Diagnostics.ParentCommandStringNotSupported(attributes.ParentCommand, commandType)); + return false; + } + + var parentCommandType = (INamedTypeSymbol)argument.Value!; + builder.AppendArgument($"parentCommandType: typeof({parentCommandType})"); + } + builder.CloseArgumentList(); builder.AppendLine(); + + return true; } private IEnumerable<(INamedTypeSymbol, ArgumentsClassAttributes?)>? GetCommands(string? assemblyName, ITypeSymbol manager) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index aff2d6e7..36b94fd9 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -315,6 +315,14 @@ public static Diagnostic CommandWithoutDescription(ISymbol symbol) => CreateDiag symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic ParentCommandStringNotSupported(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( + "OCL0015", + nameof(Resources.ParentCommandStringNotSupportedTitle), + nameof(Resources.ParentCommandStringNotSupportedMessageFormat), + DiagnosticSeverity.Error, + attribute.GetLocation(), + symbol.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index a311e936..d103c60e 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -564,6 +564,24 @@ internal static string NonRequiredInitOnlyPropertyTitle { } } + /// + /// Looks up a localized string similar to The subcommand defined by {0} uses the ParentCommandAttribute with a string argument, which is not supported by the GeneratedParserAttribute. Use a Type argument 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.. /// @@ -636,6 +654,15 @@ internal static string ShortAliasWithoutShortNameTitle { } } + /// + /// Looks up a localized string similar to . + /// + internal static string String1 { + get { + return ResourceManager.GetString("String1", resourceCulture); + } + } + /// /// Looks up a localized string similar to The type {0} must be a reference type (class) when the {1} attribute is used.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 5ea5eb25..ae923cce 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -285,6 +285,12 @@ Init accessors may only be used on required properties. + + The subcommand defined by {0} uses the ParentCommandAttribute with a string argument, which is not supported by the GeneratedParserAttribute. Use a Type argument 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. @@ -309,6 +315,9 @@ 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. diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index 47b1a626..83372994 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -87,4 +87,6 @@ public TypeHelper(Compilation compilation) public INamedTypeSymbol? ICommandProvider => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ICommandProvider"); + public INamedTypeSymbol? ParentCommandAttribute => _compilation.GetTypeByMetadataName(NamespacePrefix + "Commands.ParentCommandAttribute"); + } diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index f70b399a..e8047cf7 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -114,3 +114,42 @@ public int Run() throw new NotImplementedException(); } } + +[Command(IsHidden = true)] +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))] +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/SubCommandTest.Usage.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs new file mode 100644 index 00000000..f1e42ffc --- /dev/null +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs @@ -0,0 +1,132 @@ +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 _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 = @"Usage: test TestParentCommand [arguments] + +The following commands are available: + + NestedParentCommand + + OtherTestChildCommand + + TestChildCommand + +Run 'test TestParentCommand -Help' for more information about a command. +".ReplaceLineEndings(); + + public static readonly string _expectedNestedParentCommandUsage = @"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 = @"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 923a4802..976f5c60 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -11,520 +11,489 @@ using System.Reflection; using System.Threading.Tasks; -namespace Ookii.CommandLine.Tests -{ - [TestClass] - public class SubCommandTest - { - private static readonly Assembly _commandAssembly = Assembly.GetExecutingAssembly(); - - [ClassInitialize] - public static void TestFixtureSetup(TestContext context) - { - // Avoid exception when testing reflection on argument types that also have the - // GeneratedParseAttribute set. - ParseOptions.AllowReflectionWithGeneratedParserDefault = true; - } - - [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("version", null) - ); - } +namespace Ookii.CommandLine.Tests; - [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() { CommandNameComparer = StringComparer.Ordinal }); - 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); - } - - [TestMethod] - public void IsCommandTest() - { - bool isCommand = CommandInfo.IsCommand(typeof(TestCommand)); - Assert.IsTrue(isCommand); - - isCommand = CommandInfo.IsCommand(typeof(NotACommand)); - Assert.IsFalse(isCommand); - } +[TestClass] +public partial class SubCommandTest +{ + private static readonly Assembly _commandAssembly = Assembly.GetExecutingAssembly(); - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void CreateCommandTest(ProviderKind kind) - { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - var options = new CommandOptions() - { - Error = writer, - UsageWriter = new UsageWriter(writer) - { - ExecutableName = _executableName, - } - }; - - var manager = CreateManager(kind, 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()); + [ClassInitialize] + public static void TestFixtureSetup(TestContext context) + { + // Avoid exception when testing reflection on argument types that also have the + // GeneratedParseAttribute set. + ParseOptions.AllowReflectionWithGeneratedParserDefault = true; + } - } + [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] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsage(ProviderKind kind) - { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - var options = new CommandOptions() - { - Error = writer, - UsageWriter = new UsageWriter(writer) - { - ExecutableName = _executableName, - } - }; - - var manager = CreateManager(kind, options); - manager.WriteUsage(); - Assert.AreEqual(_expectedUsage, writer.BaseWriter.ToString()); - } + [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() { CommandNameComparer = StringComparer.Ordinal }); + 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] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageColor(ProviderKind kind) - { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - var options = new CommandOptions() - { - Error = writer, - UsageWriter = new UsageWriter(writer, true) - { - ExecutableName = _executableName, - } - }; - - var manager = CreateManager(kind, options); - manager.WriteUsage(); - Assert.AreEqual(_expectedUsageColor, writer.BaseWriter.ToString()); - } + [TestMethod] + public void IsCommandTest() + { + bool isCommand = CommandInfo.IsCommand(typeof(TestCommand)); + Assert.IsTrue(isCommand); - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageInstruction(ProviderKind kind) - { - using var writer = LineWrappingTextWriter.ForStringWriter(0); - var options = new CommandOptions() - { - Error = writer, - UsageWriter = new UsageWriter(writer) - { - ExecutableName = _executableName, - IncludeCommandHelpInstruction = true, - } - }; - - var manager = CreateManager(kind, options); - manager.WriteUsage(); - Assert.AreEqual(_expectedUsageInstruction, writer.BaseWriter.ToString()); - } + isCommand = CommandInfo.IsCommand(typeof(NotACommand)); + Assert.IsFalse(isCommand); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageApplicationDescription(ProviderKind kind) + [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) - { - IncludeApplicationDescriptionBeforeCommandList = true, - ExecutableName = _executableName, - } - }; - - var manager = CreateManager(kind, options); - manager.WriteUsage(); - Assert.AreEqual(_expectedUsageWithDescription, writer.BaseWriter.ToString()); - } + ExecutableName = _executableName, + } + }; + + var manager = CreateManager(kind, 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()); - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestCommandUsage(ProviderKind kind) - { - 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 = 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()); - } + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestCommandNameTransform(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsage(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 = CreateManager(kind, options); - var info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); - Assert.AreEqual("AnotherSimple", info.Name); - - options.CommandNameTransform = NameTransform.CamelCase; - info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); - Assert.AreEqual("anotherSimple", info.Name); - - options.CommandNameTransform = NameTransform.SnakeCase; - info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); - Assert.AreEqual("another_simple", info.Name); - - options.CommandNameTransform = NameTransform.DashCase; - info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); - Assert.AreEqual("another-simple", info.Name); - - options.StripCommandNameSuffix = null; - info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); - Assert.AreEqual("another-simple-command", info.Name); - - options.StripCommandNameSuffix = "Command"; - Assert.IsNotNull(manager.GetCommand("another-simple")); + ExecutableName = _executableName, + } + }; - // 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(_expectedUsage, writer.BaseWriter.ToString()); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestCommandFilter(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageColor(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() { - var options = new CommandOptions() + Error = writer, + UsageWriter = new UsageWriter(writer, true) { - 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")); - } - - [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); - - // RunCommand works but calls Run. - result = manager.RunCommand(new[] { "AsyncCommand", "5" }); - Assert.AreEqual(6, result); - - // RunCommandAsync works on non-async tasks. - result = await manager.RunCommandAsync(new[] { "AnotherSimpleCommand", "-Value", "5" }); - Assert.AreEqual(5, result); - } - - [TestMethod] - public async Task TestAsyncCommandBase() - { - var command = new AsyncBaseCommand(); - var actual = await command.RunAsync(); - Assert.AreEqual(42, actual); + ExecutableName = _executableName, + } + }; - // Test Run invokes RunAsync. - actual = command.Run(); - Assert.AreEqual(42, actual); - } + var manager = CreateManager(kind, options); + manager.WriteUsage(); + Assert.AreEqual(_expectedUsageColor, writer.BaseWriter.ToString()); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestExplicitAssembly(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageInstruction(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() { - if (kind == ProviderKind.Reflection) + Error = writer, + UsageWriter = new UsageWriter(writer) { - // Using the calling assembly explicitly loads all the commands, including internal, - // same as the default constructor. - var mgr = new CommandManager(_commandAssembly); - Assert.AreEqual(6, mgr.GetCommands().Count()); + ExecutableName = _executableName, + IncludeCommandHelpInstruction = true, } + }; - // 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("version", null) - ); - } - - private record struct ExpectedCommand(string Name, Type Type, bool CustomParsing = false, params string[] Aliases); - + var manager = CreateManager(kind, options); + manager.WriteUsage(); + Assert.AreEqual(_expectedUsageInstruction, 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 TestWriteUsageApplicationDescription(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); + IncludeApplicationDescriptionBeforeCommandList = true, + ExecutableName = _executableName, } + }; - Assert.AreEqual(customParsing, command.UseCustomArgumentParsing); - CollectionAssert.AreEqual(aliases ?? Array.Empty(), command.Aliases.ToArray()); - } + var manager = CreateManager(kind, options); + manager.WriteUsage(); + Assert.AreEqual(_expectedUsageWithDescription, writer.BaseWriter.ToString()); + } - private static void VerifyCommands(IEnumerable actual, params ExpectedCommand[] expected) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCommandUsage(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() { - Assert.AreEqual(expected.Length, actual.Count()); - var index = 0; - foreach (var command in actual) + Error = writer, + UsageWriter = new UsageWriter(writer) { - var info = expected[index]; - VerifyCommand(command, info.Name, info.Type, info.CustomParsing, info.Aliases); - ++index; + 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()); + } - public static CommandManager CreateManager(ProviderKind kind, CommandOptions options = null) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestCommandNameTransform(ProviderKind kind) + { + var options = new CommandOptions() { - var manager = kind switch - { - ProviderKind.Reflection => new CommandManager(options), - ProviderKind.Generated => new GeneratedManager(options), - _ => throw new InvalidOperationException() - }; - - Assert.AreEqual(kind, manager.ProviderKind); - return manager; - } + CommandNameTransform = NameTransform.PascalCase + }; - public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data) - => $"{methodInfo.Name} ({data[0]})"; + var manager = CreateManager(kind, options); + var info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); + Assert.AreEqual("AnotherSimple", info.Name); + options.CommandNameTransform = NameTransform.CamelCase; + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); + Assert.AreEqual("anotherSimple", info.Name); - public static IEnumerable ProviderKinds - => new[] - { - new object[] { ProviderKind.Reflection }, - new object[] { ProviderKind.Generated } - }; - - #region Expected usage - - 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. + options.CommandNameTransform = NameTransform.SnakeCase; + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); + Assert.AreEqual("another_simple", info.Name); - version - Displays version information. + options.CommandNameTransform = NameTransform.DashCase; + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); + Assert.AreEqual("another-simple", info.Name); -".ReplaceLineEndings(); + options.StripCommandNameSuffix = null; + info = CommandInfo.Create(typeof(AnotherSimpleCommand), manager); + Assert.AreEqual("another-simple-command", info.Name); - public static readonly string _expectedUsageNoVersion = @"Usage: test [arguments] + options.StripCommandNameSuffix = "Command"; + Assert.IsNotNull(manager.GetCommand("another-simple")); -The following commands are available: - - AnotherSimpleCommand, alias - - custom - Custom parsing command. - - test - Test command description. - -".ReplaceLineEndings(); - - public static readonly string _expectedUsageColor = @"Usage: test [arguments] + // Check automatic command name is affected too. + options.CommandNameTransform = NameTransform.PascalCase; + Assert.AreEqual("Version", manager.GetCommand("Version")?.Name); + } -The following commands are available: + [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")); + } - AnotherSimpleCommand, alias + [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); - custom - Custom parsing command. + // RunCommand works but calls Run. + result = manager.RunCommand(new[] { "AsyncCommand", "5" }); + Assert.AreEqual(6, result); - test - Test command description. + // RunCommandAsync works on non-async tasks. + result = await manager.RunCommandAsync(new[] { "AnotherSimpleCommand", "-Value", "5" }); + Assert.AreEqual(5, result); + } - version - Displays version information. + [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 _expectedUsageInstruction = @"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) + }; - custom - Custom parsing command. + 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) } + ); - test - Test command description. + var command = manager.GetCommand("TestChildCommand"); + Assert.IsNotNull(command); - version - Displays version information. + command = manager.GetCommand("version"); + Assert.IsNull(command); -Run 'test -Help' for more information about a command. -".ReplaceLineEndings(); + command = manager.GetCommand("test"); + Assert.IsNull(command); - public static readonly string _expectedUsageWithDescription = @"Tests for Ookii.CommandLine. + manager.Options.ParentCommand = null; + var result = manager.RunCommand(new[] { "TestParentCommand", "TestChildCommand", "-Value", "5" }); + Assert.AreEqual(5, result); + } -Usage: test [arguments] + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestParentCommandUsage(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() + { + Error = writer, + UsageWriter = new UsageWriter(writer) + { + ExecutableName = _executableName, + IncludeCommandHelpInstruction = 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", "-Help" }); + Assert.AreEqual(1, result); + Assert.AreEqual(_expectedNestedChildCommandUsage, writer.ToString()); + } -The following commands are available: - 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/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 9b0a9aef..6965ab36 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1318,7 +1318,7 @@ internal static bool ShouldIndent(LineWrappingTextWriter writer) return writer.MaximumLineLength is 0 or >= 30; } - private static void WriteError(ParseOptions options, string message, string color, bool blankLine = false) + internal static void WriteError(ParseOptions options, string message, string color, bool blankLine = false) { using var errorVtSupport = options.EnableErrorColor(); try diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index 7825c857..5b8bc4d8 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -28,6 +28,7 @@ public abstract class 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. /// @@ -37,12 +38,13 @@ public abstract class CommandInfo /// /// is not a command type. /// - protected CommandInfo(Type commandType, CommandAttribute attribute, CommandManager manager) + protected CommandInfo(Type commandType, CommandAttribute attribute, CommandManager manager, Type? parentCommandType) { _manager = manager ?? throw new ArgumentNullException(nameof(manager)); _name = GetName(attribute, commandType, manager.Options); _commandType = commandType; _attribute = attribute; + ParentCommandType = parentCommandType; } internal CommandInfo(Type commandType, string name, CommandManager manager) @@ -133,6 +135,8 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// public abstract IEnumerable Aliases { get; } + public Type? ParentCommandType { get; } + /// /// Creates an instance of the command type. /// diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index 39da3a0c..0ff6a931 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -228,10 +228,14 @@ public IEnumerable GetCommands() { var commands = GetCommandsUnsortedAndFiltered(); if (_options.AutoVersionCommand && + _options.ParentCommand == null && !commands.Any(c => _options.CommandNameComparer.Compare(c.Name, Properties.Resources.AutomaticVersionCommandName) == 0)) { var versionCommand = CommandInfo.GetAutomaticVersionCommand(this); - commands = commands.Append(versionCommand); + if (Options.CommandFilter?.Invoke(versionCommand) ?? true) + { + commands = commands.Append(versionCommand); + } } return commands.OrderBy(c => c.Name, _options.CommandNameComparer); @@ -287,9 +291,14 @@ public IEnumerable GetCommands() } if (_options.AutoVersionCommand && + _options.ParentCommand == null && _options.CommandNameComparer.Compare(commandName, _options.AutoVersionCommandName()) == 0) { - return CommandInfo.GetAutomaticVersionCommand(this); + command = CommandInfo.GetAutomaticVersionCommand(this); + if (_options.CommandFilter?.Invoke(command) ?? true) + { + return command; + } } return null; @@ -457,6 +466,45 @@ public IEnumerable GetCommands() 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. + /// + /// + 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. + /// + /// + /// + /// This function creates the command by invoking the , + /// method and then invokes the method on the command. + /// + /// + 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 @@ -494,6 +542,35 @@ public IEnumerable GetCommands() return RunCommand(Environment.GetCommandLineArgs(), 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. + /// + /// + public async Task RunCommandAsync(string? commandName, ReadOnlyMemory args) + { + var command = CreateCommand(commandName, args); + if (command is IAsyncCommand asyncCommand) + { + return await asyncCommand.RunAsync(); + } + + return command?.Run(); + } + /// /// /// Finds and instantiates the subcommand with the specified name, and if it succeeds, @@ -523,6 +600,30 @@ public IEnumerable GetCommands() 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. + /// + /// + /// + /// 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(ReadOnlyMemory args) + { + var command = CreateCommand(args); + if (command is IAsyncCommand asyncCommand) + { + return await asyncCommand.RunAsync(); + } + + return command?.Run(); + } + /// /// /// Finds and instantiates the subcommand with the specified name, and if it succeeds, @@ -616,7 +717,7 @@ public string GetUsage() private IEnumerable GetCommandsUnsortedAndFiltered() { - var commands = _provider.GetCommandsUnsorted(this); + var commands = _provider.GetCommandsUnsorted(this).Where(c => c.ParentCommandType == _options.ParentCommand); if (_options.CommandFilter != null) { commands = commands.Where(c => _options.CommandFilter(c)); diff --git a/src/Ookii.CommandLine/Commands/CommandOptions.cs b/src/Ookii.CommandLine/Commands/CommandOptions.cs index 8d121f57..d1f88f8d 100644 --- a/src/Ookii.CommandLine/Commands/CommandOptions.cs +++ b/src/Ookii.CommandLine/Commands/CommandOptions.cs @@ -91,6 +91,8 @@ public class CommandOptions : ParseOptions /// public Func? CommandFilter { get; set; } + public Type? ParentCommand { get; set; } + /// /// Gets or sets a value that indicates whether a version command should automatically be /// created. diff --git a/src/Ookii.CommandLine/Commands/ParentCommand.cs b/src/Ookii.CommandLine/Commands/ParentCommand.cs new file mode 100644 index 00000000..32ff643b --- /dev/null +++ b/src/Ookii.CommandLine/Commands/ParentCommand.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ookii.CommandLine.Commands; + +public abstract class ParentCommand : ICommandWithCustomParsing, IAsyncCommand +{ + private ICommand? _childCommand; + + protected virtual int FailureExitCode { get; } = 1; + + public void Parse(ReadOnlyMemory args, CommandManager manager) + { + 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.DuplicateArguments == ErrorMode.Warning) + { + handler = (sender, e) => + { + OnDuplicateArgumentWarning(e.Argument); + }; + + parser.DuplicateArgument += handler; + } + + try + { + _childCommand = (ICommand?)parser.Parse(args.Span); + } + catch (CommandLineArgumentException) + { + // Handled by OnAfterParsing. + } + + OnAfterParsing(parser, _childCommand); + } + finally + { + manager.Options.UsageWriter.CommandName = originalCommandName; + } + } + + public virtual int Run() + { + if (_childCommand == null) + { + return FailureExitCode; + } + + return _childCommand.Run(); + } + + public virtual async Task RunAsync() + { + if (_childCommand == null) + { + return FailureExitCode; + } + + if (_childCommand is IAsyncCommand asyncCommand) + { + return await asyncCommand.RunAsync(); + } + + return _childCommand.Run(); + } + + protected virtual void OnChildCommandNotFound(string? commandName, CommandManager manager) + { + manager.WriteUsage(); + } + + protected virtual void OnDuplicateArgumentWarning(CommandLineArgument argument) + { + var parser = argument.Parser; + var warning = parser.StringProvider.DuplicateArgumentWarning(argument.ArgumentName); + CommandLineParser.WriteError(parser.Options, warning, parser.Options.WarningColor); + } + + 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..818ef804 --- /dev/null +++ b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs @@ -0,0 +1,25 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Ookii.CommandLine.Commands; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class ParentCommandAttribute : Attribute +{ + public ParentCommandAttribute(string parentCommandTypeName) + { + ParentCommandTypeName = parentCommandTypeName ?? throw new ArgumentNullException(nameof(parentCommandTypeName)); + } + + public ParentCommandAttribute(Type parentCommandType) + { + ParentCommandTypeName = parentCommandType?.AssemblyQualifiedName ?? throw new ArgumentNullException(nameof(parentCommandType)); + } + + public string ParentCommandTypeName { get; } + +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandManagerAttribute instead.")] +#endif + internal Type GetParentCommandType() => Type.GetType(ParentCommandTypeName, true)!; +} diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs index 503bcedc..85c72f6d 100644 --- a/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs @@ -24,13 +24,15 @@ public class GeneratedCommandInfo : CommandInfo /// /// /// + /// public GeneratedCommandInfo(CommandManager manager, Type commandType, CommandAttribute attribute, DescriptionAttribute? descriptionAttribute = null, IEnumerable? aliasAttributes = null, - Func? createParser = null) - : base(commandType, attribute, manager) + Func? createParser = null, + Type? parentCommandType = null) + : base(commandType, attribute, manager, parentCommandType) { _descriptionAttribute = descriptionAttribute; _aliases = aliasAttributes?.Select(a => a.Alias); diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs index e61bc2e5..fd6d5b8d 100644 --- a/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs @@ -13,8 +13,9 @@ public class GeneratedCommandInfoWithCustomParsing : GeneratedCommandInfo public GeneratedCommandInfoWithCustomParsing(CommandManager manager, CommandAttribute attribute, DescriptionAttribute? descriptionAttribute = null, - IEnumerable? aliasAttributes = null) - : base(manager, typeof(T), attribute, descriptionAttribute, aliasAttributes) + IEnumerable? aliasAttributes = null, + Type? parentCommandType = null) + : base(manager, typeof(T), attribute, descriptionAttribute, aliasAttributes, parentCommandType: parentCommandType) { } diff --git a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs index 99dac15b..c80a5e82 100644 --- a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs @@ -21,7 +21,7 @@ internal class ReflectionCommandInfo : CommandInfo private string? _description; public ReflectionCommandInfo(Type commandType, CommandAttribute? attribute, CommandManager manager) - : base(commandType, attribute ?? GetCommandAttributeOrThrow(commandType), manager) + : base(commandType, attribute ?? GetCommandAttributeOrThrow(commandType), manager, GetParentCommand(commandType)) { } @@ -88,4 +88,15 @@ private static CommandAttribute GetCommandAttributeOrThrow(Type commandType) { 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/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index 09073aad..7d309abb 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -1572,7 +1572,7 @@ protected virtual void WriteMoreInfoMessage() var name = ExecutableName; if (CommandName != null) { - ExecutableName += " " + CommandName; + name += " " + CommandName; } WriteLine(Resources.MoreInfoOnErrorFormat, name, arg.ArgumentNameWithPrefix); @@ -1886,7 +1886,15 @@ protected virtual void WriteCommandDescription(string description) /// /// protected virtual void WriteCommandHelpInstruction(string argumentNamePrefix, string argumentName) - => WriteLine(Resources.CommandHelpInstructionFormat, ExecutableName, argumentNamePrefix, argumentName); + { + var name = ExecutableName; + if (CommandName != null) + { + name += " " + CommandName; + } + + WriteLine(Resources.CommandHelpInstructionFormat, name, argumentNamePrefix, argumentName); + } #endregion From 278c36272b7158dd1f2e756a50a76e7d2a7cd946 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 30 May 2023 18:15:59 -0700 Subject: [PATCH 094/234] Added some parent command related XML comments. --- .../SubCommandTest.Usage.cs | 4 +- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 2 +- .../Commands/ParentCommand.cs | 100 +++++++++++++++++- .../Commands/ParentCommandAttribute.cs | 48 +++++++++ 4 files changed, 149 insertions(+), 5 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs index f1e42ffc..981c1a4e 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs @@ -123,7 +123,9 @@ Displays this help message. Run 'test TestParentCommand NestedParentCommand -Help' for more information about a command. ".ReplaceLineEndings(); - public static readonly string _expectedNestedChildCommandUsage = @"Usage: test TestParentCommand NestedParentCommand NestedParentChildCommand [-Help] + public static readonly string _expectedNestedChildCommandUsage = @"Unknown argument name 'Foo'. + +Usage: test TestParentCommand NestedParentCommand NestedParentChildCommand [-Help] -Help [] (-?, -h) Displays this help message. diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 976f5c60..6efd7d08 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -435,7 +435,7 @@ public void TestParentCommandUsage(ProviderKind kind) Assert.AreEqual(_expectedNestedParentCommandUsage, writer.ToString()); ((StringWriter)writer.BaseWriter).GetStringBuilder().Clear(); - result = manager.RunCommand(new[] { "TestParentCommand", "NestedParentCommand", "NestedParentChildCommand", "-Help" }); + result = manager.RunCommand(new[] { "TestParentCommand", "NestedParentCommand", "NestedParentChildCommand", "-Foo" }); Assert.AreEqual(1, result); Assert.AreEqual(_expectedNestedChildCommandUsage, writer.ToString()); } diff --git a/src/Ookii.CommandLine/Commands/ParentCommand.cs b/src/Ookii.CommandLine/Commands/ParentCommand.cs index 32ff643b..ca0e64c7 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommand.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommand.cs @@ -6,14 +6,44 @@ namespace Ookii.CommandLine.Commands; +/// +/// Base class for subcommands that have nested subcommands. +/// +/// +/// +/// The , along with the class, +/// 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. +/// +/// +/// public abstract class ParentCommand : ICommandWithCustomParsing, IAsyncCommand { private ICommand? _childCommand; - protected virtual int FailureExitCode { get; } = 1; + /// + /// Gets the exit code to return 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; + /// public void Parse(ReadOnlyMemory args, CommandManager manager) { + OnModifyOptions(manager.Options); var originalParentCommand = manager.Options.ParentCommand; manager.Options.ParentCommand = GetType(); CommandInfo? info; @@ -52,7 +82,7 @@ public void Parse(ReadOnlyMemory args, CommandManager manager) { handler = (sender, e) => { - OnDuplicateArgumentWarning(e.Argument); + OnDuplicateArgumentWarning(e.Argument, e.NewValue); }; parser.DuplicateArgument += handler; @@ -75,6 +105,7 @@ public void Parse(ReadOnlyMemory args, CommandManager manager) } } + /// public virtual int Run() { if (_childCommand == null) @@ -85,6 +116,7 @@ public virtual int Run() return _childCommand.Run(); } + /// public virtual async Task RunAsync() { if (_childCommand == null) @@ -100,18 +132,80 @@ public virtual async Task 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(); } - protected virtual void OnDuplicateArgumentWarning(CommandLineArgument argument) + /// + /// Method called when the property is set to + /// and a duplicate argument value was encountered. + /// + /// The duplicate argument. + /// The new value for the argument. + /// + /// + /// 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 void OnDuplicateArgumentWarning(CommandLineArgument argument, string? newValue) { var parser = argument.Parser; var warning = parser.StringProvider.DuplicateArgumentWarning(argument.ArgumentName); CommandLineParser.WriteError(parser.Options, warning, parser.Options.WarningColor); } + /// + /// Function called after parsing, on both success, cancellation, and failure. + /// + /// + /// The 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) diff --git a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs index 818ef804..8395c748 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs @@ -3,19 +3,67 @@ 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 nested +/// subcommand classes. 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 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 From 08117a129817d94a8f45dff9ebe1dc89191a6b9e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 31 May 2023 16:11:46 -0700 Subject: [PATCH 095/234] More XML comments for nested commands. --- src/Ookii.CommandLine/Commands/CommandInfo.cs | 17 ++ .../Commands/CommandManager.cs | 157 +++++++++++++++++- .../Commands/CommandOptions.cs | 19 +++ .../Commands/ParentCommand.cs | 5 + 4 files changed, 196 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index 5b8bc4d8..170756d2 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -135,6 +135,23 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// public abstract IEnumerable Aliases { get; } + /// + /// 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; } /// diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index 0ff6a931..4ccf8ff5 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -220,6 +220,13 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// predicate are not returned. /// /// + /// If the 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 added if the /// property is and there is no command with a conflicting name. /// @@ -269,6 +276,13 @@ public IEnumerable GetCommands() /// predicate are not returned. /// /// + /// If the 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 matches the /// name of the automatic version command, and not any other command name. @@ -332,6 +346,13 @@ public IEnumerable GetCommands() /// predicate are not returned. /// /// + /// If the 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. @@ -459,6 +480,17 @@ public IEnumerable GetCommands() /// 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, int index) { @@ -481,6 +513,17 @@ public IEnumerable GetCommands() /// 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) { @@ -498,6 +541,17 @@ public IEnumerable GetCommands() /// 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) { @@ -515,6 +569,17 @@ public IEnumerable GetCommands() /// 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, int index = 0) { @@ -535,6 +600,17 @@ public IEnumerable GetCommands() /// 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() { @@ -559,6 +635,17 @@ public IEnumerable GetCommands() /// 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 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 async Task RunCommandAsync(string? commandName, ReadOnlyMemory args) { @@ -588,6 +675,17 @@ public IEnumerable GetCommands() /// 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 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 async Task RunCommandAsync(string? commandName, string[] args, int index) { @@ -612,6 +710,17 @@ public IEnumerable GetCommands() /// 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 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 async Task RunCommandAsync(ReadOnlyMemory args) { @@ -636,6 +745,17 @@ public IEnumerable GetCommands() /// 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 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 async Task RunCommandAsync(string[] args, int index = 0) { @@ -661,6 +781,17 @@ public IEnumerable GetCommands() /// 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 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 async Task RunCommandAsync() { @@ -678,13 +809,24 @@ public IEnumerable GetCommands() /// /// /// - /// This method writes usage help for the application, including a list of all shell - /// command names and their descriptions to . + /// This method writes usage help for the application, including a list of all + /// subcommand names and their descriptions to . /// /// /// 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 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 void WriteUsage() { @@ -700,6 +842,17 @@ public void WriteUsage() /// 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 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 string GetUsage() { diff --git a/src/Ookii.CommandLine/Commands/CommandOptions.cs b/src/Ookii.CommandLine/Commands/CommandOptions.cs index d1f88f8d..0868d944 100644 --- a/src/Ookii.CommandLine/Commands/CommandOptions.cs +++ b/src/Ookii.CommandLine/Commands/CommandOptions.cs @@ -91,6 +91,25 @@ public class CommandOptions : ParseOptions /// public Func? CommandFilter { get; set; } + /// + /// 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 command manager. + /// + /// public Type? ParentCommand { get; set; } /// diff --git a/src/Ookii.CommandLine/Commands/ParentCommand.cs b/src/Ookii.CommandLine/Commands/ParentCommand.cs index ca0e64c7..e9987c67 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommand.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommand.cs @@ -25,6 +25,11 @@ namespace Ookii.CommandLine.Commands; /// 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 +/// attribute, so derived classes cannot define any arguments or use other functionality that +/// depends on the class. +/// /// /// public abstract class ParentCommand : ICommandWithCustomParsing, IAsyncCommand From d59b8f05a477f7bee8f90d28d2795493e8b03d02 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 31 May 2023 16:23:46 -0700 Subject: [PATCH 096/234] Don't emit ignored attribute warning for compiler attributes. --- src/Ookii.CommandLine.Generator/ArgumentAttributes.cs | 10 ++++++---- .../ArgumentsClassAttributes.cs | 6 ++++-- src/Ookii.CommandLine.Generator/CommandGenerator.cs | 2 +- src/Ookii.CommandLine.Generator/Diagnostics.cs | 5 +++-- src/Ookii.CommandLine.Generator/ParserGenerator.cs | 2 +- .../Properties/Resources.Designer.cs | 11 +---------- .../Properties/Resources.resx | 5 +---- 7 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs index 867b831a..483ed165 100644 --- a/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs +++ b/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs @@ -17,9 +17,9 @@ internal class ArgumentAttributes private readonly List? _shortAliases; private readonly List? _validators; - public ArgumentAttributes(IEnumerable attributes, TypeHelper typeHelper, SourceProductionContext context) + public ArgumentAttributes(ISymbol member, TypeHelper typeHelper, SourceProductionContext context) { - foreach (var attribute in attributes) + foreach (var attribute in member.GetAttributes()) { if (attribute.CheckType(typeHelper.CommandLineArgumentAttribute, ref _commandLineArgumentAttribute) || attribute.CheckType(typeHelper.MultiValueSeparatorAttribute, ref _multiValueSeparator) || @@ -32,12 +32,14 @@ public ArgumentAttributes(IEnumerable attributes, TypeHelper type attribute.CheckType(typeHelper.ValueConverterAttribute, ref _valueConverterAttribute) || attribute.CheckType(typeHelper.AliasAttribute, ref _aliases) || attribute.CheckType(typeHelper.ShortAliasAttribute, ref _shortAliases) || - attribute.CheckType(typeHelper.ArgumentValidationAttribute, ref _validators)) + attribute.CheckType(typeHelper.ArgumentValidationAttribute, ref _validators) || + // Don't warn about attributes used by the compiler. + (attribute.AttributeClass?.ContainingNamespace.ToDisplayString().StartsWith("System.Runtime.CompilerServices") ?? false)) { continue; } - context.ReportDiagnostic(Diagnostics.IgnoredAttribute(attribute)); + context.ReportDiagnostic(Diagnostics.IgnoredAttribute(member, attribute)); } } diff --git a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs index c5fc6c2e..1748e079 100644 --- a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs +++ b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs @@ -27,14 +27,16 @@ public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper, Sourc attribute.CheckType(typeHelper.ClassValidationAttribute, ref _classValidators) || attribute.CheckType(typeHelper.ParentCommandAttribute, ref _parentCommand) || attribute.CheckType(typeHelper.AliasAttribute, ref _aliases) || - attribute.CheckType(typeHelper.GeneratedParserAttribute, ref _generatedParser)) + attribute.CheckType(typeHelper.GeneratedParserAttribute, ref _generatedParser) || + // Don't warn about attributes used by the compiler. + (attribute.AttributeClass?.ContainingNamespace.ToDisplayString().StartsWith("System.Runtime.CompilerServices") ?? false)) { continue; } if (context is SourceProductionContext c) { - c.ReportDiagnostic(Diagnostics.IgnoredAttribute(attribute)); + c.ReportDiagnostic(Diagnostics.IgnoredAttribute(current, attribute)); } } } diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index ed2eabfa..8ba688b0 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -177,7 +177,7 @@ private bool GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType builder.AppendArgument($"typeof({commandTypeName})"); } - var attributes = commandAttributes ?? new ArgumentsClassAttributes(commandType, _typeHelper, _context); + var attributes = commandAttributes ?? new ArgumentsClassAttributes(commandType, _typeHelper, null); builder.AppendArgument($"{attributes.Command!.CreateInstantiation()}"); if (attributes.Description != null) { diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 36b94fd9..fe443a9e 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -140,13 +140,14 @@ public static Diagnostic ArgumentConverterStringNotSupported(AttributeData attri attribute.GetLocation(), symbol.ToDisplayString()); - public static Diagnostic IgnoredAttribute(AttributeData attribute) => CreateDiagnostic( + public static Diagnostic IgnoredAttribute(ISymbol symbol, AttributeData attribute) => CreateDiagnostic( "OCL0016", nameof(Resources.UnknownAttributeTitle), nameof(Resources.UnknownAttributeMessageFormat), DiagnosticSeverity.Warning, attribute.GetLocation(), - attribute.AttributeClass?.Name); + attribute.AttributeClass?.ToDisplayString(), + symbol.ToDisplayString()); public static Diagnostic NonPublicStaticMethod(ISymbol method) => CreateDiagnostic( "OCL0017", diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index e3b558ca..eef131c6 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -239,7 +239,7 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> return true; } - var attributes = new ArgumentAttributes(member.GetAttributes(), _typeHelper, _context); + var attributes = new ArgumentAttributes(member, _typeHelper, _context); // Check if it is an argument. if (attributes.CommandLineArgument == null) diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index d103c60e..3def6fbc 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -654,15 +654,6 @@ internal static string ShortAliasWithoutShortNameTitle { } } - /// - /// Looks up a localized string similar to . - /// - internal static string String1 { - get { - return ResourceManager.GetString("String1", resourceCulture); - } - } - /// /// Looks up a localized string similar to The type {0} must be a reference type (class) when the {1} attribute is used.. /// @@ -700,7 +691,7 @@ internal static string UnknownAssemblyNameTitle { } /// - /// Looks up a localized string similar to The attribute {0} is unknown and will be ignored by the GeneratedParserAttribute.. + /// Looks up a localized string similar to The attribute '{0}' on '{1}' is unknown and will be ignored by the GeneratedParserAttribute.. /// internal static string UnknownAttributeMessageFormat { get { diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index ae923cce..361962db 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -315,9 +315,6 @@ 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. @@ -331,7 +328,7 @@ Unknown assembly name. - The attribute {0} is unknown and will be ignored by the GeneratedParserAttribute. + The attribute '{0}' on '{1}' is unknown and will be ignored by the GeneratedParserAttribute. Unknown attribute will be ignored. From 7c66a55a3b02db4f0e07d3e5964be5f345f64e80 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 31 May 2023 17:09:09 -0700 Subject: [PATCH 097/234] Update NestedCommands sample to use official support. --- src/Samples/NestedCommands/CourseCommands.cs | 6 +- .../NestedCommands/CustomUsageWriter.cs | 46 --------------- .../NestedCommands/GeneratedManager.cs | 8 +++ src/Samples/NestedCommands/ListCommand.cs | 3 +- .../NestedCommands/NestedCommands.csproj | 9 ++- src/Samples/NestedCommands/ParentCommand.cs | 57 ------------------- .../NestedCommands/ParentCommandAttribute.cs | 19 ------- src/Samples/NestedCommands/Program.cs | 35 +++++------- src/Samples/NestedCommands/StudentCommands.cs | 9 ++- 9 files changed, 41 insertions(+), 151 deletions(-) delete mode 100644 src/Samples/NestedCommands/CustomUsageWriter.cs create mode 100644 src/Samples/NestedCommands/GeneratedManager.cs delete mode 100644 src/Samples/NestedCommands/ParentCommand.cs delete mode 100644 src/Samples/NestedCommands/ParentCommandAttribute.cs diff --git a/src/Samples/NestedCommands/CourseCommands.cs b/src/Samples/NestedCommands/CourseCommands.cs index 354d41c5..8a4bfc3e 100644 --- a/src/Samples/NestedCommands/CourseCommands.cs +++ b/src/Samples/NestedCommands/CourseCommands.cs @@ -18,7 +18,8 @@ internal class CourseCommand : ParentCommand [Command("add")] [ParentCommand(typeof(CourseCommand))] [Description("Adds a course to the database.")] -internal class AddCourseCommand : BaseCommand +[GeneratedParser] +internal partial class AddCourseCommand : BaseCommand { [CommandLineArgument(Position = 0, IsRequired = true)] [Description("The name of the course.")] @@ -45,7 +46,8 @@ protected override async Task RunAsync(Database db) [Command("remove")] [ParentCommand(typeof(CourseCommand))] [Description("Removes a course from the database.")] -internal class RemoveCourseCommand : BaseCommand +[GeneratedParser] +internal partial class RemoveCourseCommand : BaseCommand { [CommandLineArgument(Position = 0, IsRequired = true)] 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..9cb3512f --- /dev/null +++ b/src/Samples/NestedCommands/GeneratedManager.cs @@ -0,0 +1,8 @@ +using Ookii.CommandLine.Commands; + +namespace NestedCommands; + +[GeneratedCommandManager] +internal partial class GeneratedManager +{ +} diff --git a/src/Samples/NestedCommands/ListCommand.cs b/src/Samples/NestedCommands/ListCommand.cs index a14e7835..36dd2ed2 100644 --- a/src/Samples/NestedCommands/ListCommand.cs +++ b/src/Samples/NestedCommands/ListCommand.cs @@ -8,7 +8,8 @@ namespace NestedCommands; // BaseCommand, it has a Path argument even though no arguments are defined here [Command("list")] [Description("Lists all students and courses.")] -internal class ListCommand : BaseCommand +[GeneratedParser] +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..30fdfd20 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. @@ -14,4 +14,11 @@ 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 dd4fa732..00000000 --- a/src/Samples/NestedCommands/ParentCommand.cs +++ /dev/null @@ -1,57 +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(ReadOnlyMemory args, CommandManager manager) - { - // Nested commands don't need to have a "version" command. - manager.Options.AutoVersionCommand = false; - - // Select only the commands that have a ParentCommandAttribute specifying this command - // as their parent. - manager.Options.CommandFilter = - (command) => command.CommandType.GetCustomAttribute()?.ParentCommand == GetType(); - - var info = CommandInfo.Create(GetType(), manager); - - // Use a custom UsageWriter to replace the application description with the - // description of this command. - manager.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); - } - - 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..bd1cb4cc 100644 --- a/src/Samples/NestedCommands/Program.cs +++ b/src/Samples/NestedCommands/Program.cs @@ -1,27 +1,18 @@ -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, + // 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, + }, +}; - 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/StudentCommands.cs b/src/Samples/NestedCommands/StudentCommands.cs index 760d2ce0..f2d918ab 100644 --- a/src/Samples/NestedCommands/StudentCommands.cs +++ b/src/Samples/NestedCommands/StudentCommands.cs @@ -18,7 +18,8 @@ internal class StudentCommand : ParentCommand [Command("add")] [ParentCommand(typeof(StudentCommand))] [Description("Adds a student to the database.")] -internal class AddStudentCommand : BaseCommand +[GeneratedParser] +internal partial class AddStudentCommand : BaseCommand { [CommandLineArgument(Position = 0, IsRequired = true)] [Description("The first name of the student.")] @@ -49,7 +50,8 @@ protected override async Task RunAsync(Database db) [Command("remove")] [ParentCommand(typeof(StudentCommand))] [Description("Removes a student from the database.")] -internal class RemoveStudentCommand : BaseCommand +[GeneratedParser] +internal partial class RemoveStudentCommand : BaseCommand { [CommandLineArgument(Position = 0, IsRequired = true)] @@ -75,7 +77,8 @@ protected override async Task RunAsync(Database db) [Command("add-course")] [ParentCommand(typeof(StudentCommand))] [Description("Adds a course for a student.")] -internal class AddStudentCourseCommand : BaseCommand +[GeneratedParser] +internal partial class AddStudentCourseCommand : BaseCommand { [CommandLineArgument(Position = 0, IsRequired = true)] [Description("The ID of the student.")] From 23179cfd49d83a2aeb6a70a32151d1b87cd260be Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 31 May 2023 17:58:35 -0700 Subject: [PATCH 098/234] Use command description instead of application description for parent command usage. --- src/Ookii.CommandLine.Tests/CommandTypes.cs | 2 ++ .../SubCommandTest.Usage.cs | 9 +++++++-- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 1 + .../Commands/CommandManager.cs | 18 +++++++++++++++--- src/Ookii.CommandLine/UsageWriter.cs | 6 ++++++ 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index e8047cf7..6a7f23b6 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -116,6 +116,7 @@ public int Run() } [Command(IsHidden = true)] +[Description("Parent command description.")] class TestParentCommand : ParentCommand { } @@ -141,6 +142,7 @@ partial class OtherTestChildCommand : ICommand [Command] [ParentCommand(typeof(TestParentCommand))] +[Description("Other parent command description.")] class NestedParentCommand : ParentCommand { } diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs index 981c1a4e..597221ea 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs @@ -101,11 +101,14 @@ Displays this help message. ".ReplaceLineEndings(); - public static readonly string _expectedParentCommandUsage = @"Usage: test TestParentCommand [arguments] + public static readonly string _expectedParentCommandUsage = @"Parent command description. + +Usage: test TestParentCommand [arguments] The following commands are available: NestedParentCommand + Other parent command description. OtherTestChildCommand @@ -114,7 +117,9 @@ Displays this help message. Run 'test TestParentCommand -Help' for more information about a command. ".ReplaceLineEndings(); - public static readonly string _expectedNestedParentCommandUsage = @"Usage: test TestParentCommand NestedParentCommand [arguments] + public static readonly string _expectedNestedParentCommandUsage = @"Other parent command description. + +Usage: test TestParentCommand NestedParentCommand [arguments] The following commands are available: diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 6efd7d08..f316d7ec 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -421,6 +421,7 @@ public void TestParentCommandUsage(ProviderKind kind) { ExecutableName = _executableName, IncludeCommandHelpInstruction = true, + IncludeApplicationDescriptionBeforeCommandList = true, } }; diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index 4ccf8ff5..94637314 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -863,10 +863,22 @@ public string GetUsage() /// 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. + /// 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() => _provider.GetApplicationDescription(); + /// + public string? GetApplicationDescription() + { + var attribute = _options.ParentCommand?.GetCustomAttribute(); + if (attribute != null) + { + return attribute.Description; + } + + return _provider.GetApplicationDescription(); + } private IEnumerable GetCommandsUnsortedAndFiltered() { diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index 7d309abb..277e5370 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -4,6 +4,7 @@ using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -565,6 +566,11 @@ public bool IncludeExecutableExtension /// 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. + /// /// public bool IncludeApplicationDescriptionBeforeCommandList { get; set; } From bc610277910cb9c13801785e3de7373c8caf16b2 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 31 May 2023 18:24:44 -0700 Subject: [PATCH 099/234] Process all commands/arguments on error to report all errors. --- .../CommandGenerator.cs | 16 ++++++++++++---- .../ParserGenerator.cs | 5 +++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index 8ba688b0..ec2a2017 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -120,6 +120,7 @@ public void Generate() builder.OpenBlock(); var generatedManagerAttribute = manager.GetAttribute(_typeHelper.GeneratedCommandManagerAttribute!)!; + var hasError = false; if (generatedManagerAttribute.GetNamedArgument("AssemblyNames") is TypedConstant assemblies) { foreach (var assembly in assemblies.Values) @@ -127,12 +128,16 @@ public void Generate() var commands = GetCommands(assembly.Value as string, manager); if (commands == null) { - return null; + hasError = true; + continue; } foreach (var (command, attributes) in commands) { - GenerateCommand(builder, command, attributes); + if (!GenerateCommand(builder, command, attributes)) + { + hasError = true; + } } } } @@ -140,7 +145,10 @@ public void Generate() { foreach (var (command, attributes) in _commands) { - GenerateCommand(builder, command, attributes); + if (!GenerateCommand(builder, command, attributes)) + { + hasError = true; + } } } @@ -154,7 +162,7 @@ public void Generate() builder.OpenBlock(); builder.CloseBlock(); // ctor builder.CloseBlock(); // manager class - return builder.GetSource(); + return hasError ? null : builder.GetSource(); } private bool GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType, ArgumentsClassAttributes? commandAttributes) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index eef131c6..514a3acb 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -169,13 +169,14 @@ private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isComman var current = _argumentsClass; List<(string, string, string)>? requiredProperties = null; + var hasError = false; while (current != null && current.SpecialType != SpecialType.System_Object) { foreach (var member in current.GetMembers()) { if (!GenerateArgument(member, ref requiredProperties)) { - return false; + hasError = true; } } @@ -228,7 +229,7 @@ private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isComman _builder.CloseBlock(); // CreateInstance() _builder.CloseBlock(); // GeneratedProvider class - return true; + return !hasError; } private bool GenerateArgument(ISymbol member, ref List<(string, string, string)>? requiredProperties) From 8c1a8f9292aae39f94726d29325a8b97ebfc1ec5 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 7 Jun 2023 10:33:21 -0700 Subject: [PATCH 100/234] Improved XML comments for source generation attributes. --- .../GeneratedCommandManagerAttribute.cs | 16 +++++++++++--- .../GeneratedParserAttribute.cs | 22 +++++++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs index d0690113..14fcb58b 100644 --- a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs +++ b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs @@ -4,12 +4,22 @@ namespace Ookii.CommandLine.Commands; /// -/// Indicates that the class with this attribute uses code generation to provide commands to a -/// class. +/// Indicates that the target class is a command manager created using source generation. /// /// -/// TODO: Better docs. +/// +/// When using this attribute, source generation is used to find and instantiate subcommand +/// classes in the current assembly, 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. +/// +/// +/// To use source generation for the command line arguments of individual commands, use the +/// attribute on the command class. +/// /// +/// Source generation [AttributeUsage(AttributeTargets.Class)] public sealed class GeneratedCommandManagerAttribute : Attribute { diff --git a/src/Ookii.CommandLine/GeneratedParserAttribute.cs b/src/Ookii.CommandLine/GeneratedParserAttribute.cs index c7c88b14..ac156deb 100644 --- a/src/Ookii.CommandLine/GeneratedParserAttribute.cs +++ b/src/Ookii.CommandLine/GeneratedParserAttribute.cs @@ -4,9 +4,27 @@ namespace Ookii.CommandLine; /// -/// Indicates that the specified arguments type should use source generation. -/// TODO: Better help. +/// 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: CreateParser, and three overloads of the Parse method. You must use +/// these members, as using the class directly will throw +/// an exception unless is set to +/// . +/// +/// +/// When using source generation with subcommands, you should also use a class with the +/// attribute to access the commands. +/// +/// +/// Source generation [AttributeUsage(AttributeTargets.Class)] public sealed class GeneratedParserAttribute : Attribute { From 7a5590e3187096bd2ed67234d03193498bb8d28d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 7 Jun 2023 10:41:45 -0700 Subject: [PATCH 101/234] Use generated parser with reflection CommandManager. --- src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs index c80a5e82..97f79feb 100644 --- a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs @@ -49,6 +49,16 @@ public override CommandLineParser CreateParser() throw new InvalidOperationException(Properties.Resources.NoParserForCustomParsingCommand); } + if (Attribute.IsDefined(CommandType, typeof(GeneratedParserAttribute))) + { + var method = CommandType.GetMethod("CreateParser", BindingFlags.Public | BindingFlags.Static); + if (method != null) + { + var parameters = new object[] { Manager.Options }; + return (CommandLineParser)(method.Invoke(null, parameters) ?? throw new InvalidOperationException("TODO")); + } + } + return new CommandLineParser(CommandType, Manager.Options); } From 55f76b5df9fa0e2e9df45f0b7bd43c7a5b2bc9a0 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 7 Jun 2023 15:38:00 -0700 Subject: [PATCH 102/234] Replaced CancelParsing with an enum. --- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 2 +- .../CommandLineParserTest.cs | 9 +- .../ArgumentParsedEventArgs.cs | 95 +++++++++---------- src/Ookii.CommandLine/CancelMode.cs | 27 ++++++ src/Ookii.CommandLine/CommandLineArgument.cs | 79 +++++---------- .../CommandLineArgumentAttribute.cs | 36 +++---- src/Ookii.CommandLine/CommandLineParser.cs | 79 ++++++++------- 7 files changed, 167 insertions(+), 160 deletions(-) create mode 100644 src/Ookii.CommandLine/CancelMode.cs diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index f8637a40..2210056e 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -183,7 +183,7 @@ partial class CancelArguments [CommandLineArgument] public bool DoesNotCancel { get; set; } - [CommandLineArgument(CancelParsing = true)] + [CommandLineArgument(CancelParsing = CancelMode.Abort)] public bool DoesCancel { get; set; } } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 9df2c3f8..bfa8b945 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -517,7 +517,7 @@ static void handler1(object sender, ArgumentParsedEventArgs e) { if (e.Argument.ArgumentName == "DoesNotCancel") { - e.Cancel = true; + e.CancelParsing = CancelMode.Abort; } } @@ -527,7 +527,7 @@ static void handler1(object sender, ArgumentParsedEventArgs e) Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.LastException); Assert.AreEqual("DoesNotCancel", parser.ParseResult.ArgumentName); - Assert.IsTrue(parser.HelpRequested); + Assert.IsFalse(parser.HelpRequested); Assert.IsTrue(parser.GetArgument("Argument1").HasValue); Assert.AreEqual("foo", (string)parser.GetArgument("Argument1").Value); Assert.IsTrue(parser.GetArgument("DoesNotCancel").HasValue); @@ -543,12 +543,15 @@ static void handler2(object sender, ArgumentParsedEventArgs e) { if (e.Argument.ArgumentName == "DoesCancel") { - e.OverrideCancelParsing = true; + Assert.AreEqual(CancelMode.Abort, e.CancelParsing); + e.CancelParsing = CancelMode.None; } } 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.IsNotNull(result); Assert.IsFalse(parser.HelpRequested); Assert.IsFalse(result.DoesNotCancel); diff --git a/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs b/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs index 83b7a456..b7a88309 100644 --- a/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs +++ b/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs @@ -1,64 +1,59 @@ // Copyright (c) Sven Groot (Ookii.org) using System; -using System.ComponentModel; -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 attribute, 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/CancelMode.cs b/src/Ookii.CommandLine/CancelMode.cs new file mode 100644 index 00000000..93bd8ab7 --- /dev/null +++ b/src/Ookii.CommandLine/CancelMode.cs @@ -0,0 +1,27 @@ +namespace Ookii.CommandLine; + +/// +/// Indicates whether and how the 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 a069e347..6fa9bed4 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -26,7 +26,7 @@ public abstract class CommandLineArgument private interface IValueHelper { object? Value { get; } - bool SetValue(CommandLineArgument argument, object? value); + CancelMode SetValue(CommandLineArgument argument, object? value); void ApplyValue(CommandLineArgument argument, object target); } @@ -44,10 +44,10 @@ public void ApplyValue(CommandLineArgument argument, object target) argument.SetProperty(target, Value); } - public bool SetValue(CommandLineArgument argument, object? value) + public CancelMode SetValue(CommandLineArgument argument, object? value) { Value = value; - return true; + return CancelMode.None; } } @@ -75,10 +75,10 @@ public void ApplyValue(CommandLineArgument argument, object target) } } - public bool SetValue(CommandLineArgument argument, object? value) + public CancelMode SetValue(CommandLineArgument argument, object? value) { _values.Add((T?)value); - return true; + return CancelMode.None; } } @@ -116,7 +116,7 @@ public void ApplyValue(CommandLineArgument argument, object target) } } - public bool SetValue(CommandLineArgument argument, object? value) + public CancelMode SetValue(CommandLineArgument argument, object? value) { // ConvertToArgumentType is guaranteed to return non-null for dictionary arguments. var pair = (KeyValuePair)value!; @@ -144,7 +144,7 @@ public bool SetValue(CommandLineArgument argument, object? value) throw argument._parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.InvalidDictionaryValue, ex, argument, value.ToString()); } - return true; + return CancelMode.None; } } @@ -157,12 +157,13 @@ public void ApplyValue(CommandLineArgument argument, object target) throw new InvalidOperationException(); } - public bool SetValue(CommandLineArgument argument, object? value) + public CancelMode SetValue(CommandLineArgument argument, object? value) { Value = value; try { - return argument.CallMethod(value); + // TODO: Support methods returning CancelMode. + return argument.CallMethod(value) ? CancelMode.None : CancelMode.Abort; } catch (TargetInvocationException ex) { @@ -199,7 +200,7 @@ private static ArgumentInfo CreateInfo(CommandLineParser parser, string argument ElementType = typeof(bool), Description = parser.StringProvider.AutomaticHelpDescription(), MemberName = "AutomaticHelp", - CancelParsing = true, + CancelParsing = CancelMode.Abort, Validators = Enumerable.Empty(), Converter = Conversion.BooleanConverter.Instance, }; @@ -286,7 +287,7 @@ internal struct ArgumentInfo public string? KeyValueSeparator { get; set; } public bool AllowDuplicateDictionaryKeys { get; set; } public bool AllowNull { get; set; } - public bool CancelParsing { get; set; } + public CancelMode CancelParsing { get; set; } public bool IsHidden { get; set; } public Type? KeyType { get; set; } public Type? ValueType { get; set; } @@ -317,7 +318,7 @@ internal struct ArgumentInfo private readonly bool _allowMultiValueWhiteSpaceSeparator; private readonly string? _keyValueSeparator; private readonly bool _allowNull; - private readonly bool _cancelParsing; + private readonly CancelMode _cancelParsing; private readonly bool _isHidden; private readonly IEnumerable _validators; private string? _valueDescription; @@ -1006,46 +1007,15 @@ public bool AllowsDuplicateDictionaryKeys /// argument is encountered. /// /// - /// if argument parsing should be canceled after this argument; - /// otherwise, . + /// One of the values of the enumeration. /// /// /// /// 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; + public CancelMode CancelParsing => _cancelParsing; /// /// Gets or sets a value that indicates whether the argument is hidden from the usage help. @@ -1389,39 +1359,38 @@ internal bool HasInformation(UsageWriter writer) return false; } - internal bool SetValue(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) + internal CancelMode SetValue(CultureInfo culture, bool hasValue, string? stringValue, ReadOnlySpan spanValue) { _valueHelper ??= CreateValueHelper(); - bool continueParsing; + CancelMode cancelParsing; if (IsMultiValue && hasValue && MultiValueSeparator != null) { - continueParsing = true; + cancelParsing = CancelMode.None; spanValue.Split(MultiValueSeparator.AsSpan(), separateValue => { string? separateValueString = null; PreValidate(ref separateValueString, separateValue); var converted = ConvertToArgumentType(culture, true, separateValueString, separateValue); - continueParsing = _valueHelper.SetValue(this, converted); - if (!continueParsing) + cancelParsing = _valueHelper.SetValue(this, converted); + if (cancelParsing != CancelMode.Abort) { - return false; + Validate(converted, ValidationMode.AfterConversion); } - Validate(converted, ValidationMode.AfterConversion); - return true; + return cancelParsing == CancelMode.None; }); } else { PreValidate(ref stringValue, spanValue); var converted = ConvertToArgumentType(culture, hasValue, stringValue, spanValue); - continueParsing = _valueHelper.SetValue(this, converted); + cancelParsing = _valueHelper.SetValue(this, converted); Validate(converted, ValidationMode.AfterConversion); } HasValue = true; - return continueParsing; + return cancelParsing; } internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParser parser) diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index c916cae1..07c1f4a5 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -243,43 +243,43 @@ public bool IsShort /// this argument is encountered. /// /// - /// if argument parsing should be canceled after this argument; - /// otherwise, . The default value is . + /// One of the values of the enumeration. /// /// /// - /// 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. + /// 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. /// /// - /// 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. + /// 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. /// /// /// The method and the /// static helper method - /// will print usage information if parsing was canceled through this method. + /// will print usage information if parsing was canceled with . /// /// /// Canceling parsing in this way is identical to handling the - /// event and setting to - /// . + /// event and setting property. /// /// /// It's possible to prevent cancellation when an argument has this property set by /// handling the event and setting the - /// property to - /// . + /// property to . /// /// /// - public bool CancelParsing { get; set; } + public CancelMode CancelParsing { get; set; } /// /// Gets or sets a value that indicates whether the argument is hidden from the usage help. diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 6965ab36..74c5e2d0 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1504,6 +1504,7 @@ private void VerifyPositionalArgumentRules() HelpRequested = false; int positionalArgumentIndex = 0; + var cancelParsing = CancelMode.None; for (int x = 0; x < args.Length; ++x) { string arg = args[x]; @@ -1512,10 +1513,10 @@ private void VerifyPositionalArgumentRules() { // 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) + (cancelParsing, x) = ParseNamedArgument(args, x, argumentNamePrefix.Value); + if (cancelParsing != CancelMode.None) { - return null; + break; } } else @@ -1537,13 +1538,19 @@ private void VerifyPositionalArgumentRules() // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler // or the CancelParsing property. - if (ParseArgumentValue(_arguments[positionalArgumentIndex], arg, arg.AsMemory())) + cancelParsing = ParseArgumentValue(_arguments[positionalArgumentIndex], arg, arg.AsMemory()); + if (cancelParsing != CancelMode.None) { - return null; + break; } } } + if (cancelParsing == CancelMode.Abort) + { + return null; + } + // Check required arguments and post-parsing validation. This is done in usage order. foreach (CommandLineArgument argument in _arguments) { @@ -1554,7 +1561,6 @@ private void VerifyPositionalArgumentRules() _provider.RunValidators(this); object commandLineArguments; - // TODO: Integrate with new ctor argument support. try { object?[]? requiredPropertyValues = null; @@ -1584,11 +1590,13 @@ private void VerifyPositionalArgumentRules() argument.ApplyPropertyValue(commandLineArguments); } + // Reset in case it was set by a method argument that didn't cancel parsing. + HelpRequested = false; ParseResult = ParseResult.Success; return commandLineArguments; } - private bool ParseArgumentValue(CommandLineArgument argument, string? stringValue, ReadOnlyMemory? memoryValue) + private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stringValue, ReadOnlyMemory? memoryValue) { if (argument.HasValue && !argument.IsMultiValue) { @@ -1604,38 +1612,43 @@ private bool ParseArgumentValue(CommandLineArgument argument, string? stringValu OnDuplicateArgument(duplicateEventArgs); if (duplicateEventArgs.KeepOldValue) { - return false; + return CancelMode.None; } } - bool continueParsing = argument.SetValue(Culture, memoryValue.HasValue, stringValue, (memoryValue ?? default).Span); + var cancelParsing = argument.SetValue(Culture, memoryValue.HasValue, stringValue, (memoryValue ?? default).Span); var e = new ArgumentParsedEventArgs(argument) { - Cancel = !continueParsing + CancelParsing = cancelParsing }; - 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) + if (argument.CancelParsing != CancelMode.None) { - HelpRequested = cancel; + e.CancelParsing = argument.CancelParsing; } - if (cancel) + OnArgumentParsed(e); + + if (e.CancelParsing != CancelMode.None) { + // Automatically request help only if the cancellation was due to the + // CommandLineArgumentAttribute.CancelParsing property. + if (argument.CancelParsing == CancelMode.Abort) + { + HelpRequested = true; + } + ParseResult = ParseResult.FromCanceled(argument.ArgumentName); } - return cancel; + return e.CancelParsing; } - private int ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo prefix) + private (CancelMode, int) ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo prefix) { var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitOnce(NameValueSeparator); + CancelMode cancelParsing; CommandLineArgument? argument = null; if (_argumentsByShortName != null && prefix.Short) { @@ -1645,9 +1658,8 @@ private int ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo } else { - // ParseShortArgument returns true if parsing was canceled by the - // ArgumentParsed event handler or the CancelParsing property. - return ParseShortArgument(argumentName.Span, argumentValue) ? -1 : index; + cancelParsing = ParseShortArgument(argumentName.Span, argumentValue); + return (cancelParsing, index); } } @@ -1677,11 +1689,10 @@ private int ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo ++index; argumentValueString = args[index]; - // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed - // event handler or the CancelParsing property. - if (ParseArgumentValue(argument, argumentValueString, argumentValueString.AsMemory())) + cancelParsing = ParseArgumentValue(argument, argumentValueString, argumentValueString.AsMemory()); + if (cancelParsing != CancelMode.None) { - return -1; + return (cancelParsing, index); } if (!argument.AllowMultiValueWhiteSpaceSeparator) @@ -1692,13 +1703,14 @@ private int ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo if (argumentValueString != null) { - return index; + return (CancelMode.None, index); } } // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler // or the CancelParsing property. - return ParseArgumentValue(argument, null, argumentValue) ? -1 : index; + cancelParsing = ParseArgumentValue(argument, null, argumentValue); + return (cancelParsing, index); } private CommandLineArgument? GetArgumentByNamePrefix(ReadOnlySpan prefix) @@ -1740,7 +1752,7 @@ private int ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo return foundArgument; } - private bool ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? value) + private CancelMode ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? value) { foreach (var ch in name) { @@ -1750,13 +1762,14 @@ private bool ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? v throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, name.ToString()); } - if (ParseArgumentValue(arg, null, value)) + var cancelParsing = ParseArgumentValue(arg, null, value); + if (cancelParsing != CancelMode.None) { - return true; + return cancelParsing; } } - return false; + return CancelMode.None; } private CommandLineArgument GetShortArgumentOrThrow(char shortName) From f0e2d7bb23b982906c088e885802c4c42bd846d3 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 7 Jun 2023 17:09:19 -0700 Subject: [PATCH 103/234] Implement remaining arguments for cancellation. --- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 3 ++ .../CommandLineParserTest.cs | 39 ++++++++++++++ src/Ookii.CommandLine/CommandLineParser.cs | 53 +++++++++++-------- src/Ookii.CommandLine/Commands/CommandInfo.cs | 2 +- .../Commands/ParentCommand.cs | 2 +- src/Ookii.CommandLine/ParseResult.cs | 35 +++++++++--- 6 files changed, 103 insertions(+), 31 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 2210056e..b98ae762 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -185,6 +185,9 @@ partial class CancelArguments [CommandLineArgument(CancelParsing = CancelMode.Abort)] public bool DoesCancel { get; set; } + + [CommandLineArgument(CancelParsing = CancelMode.Success)] + public bool DoesCancelWithSuccess { get; set; } } [GeneratedParser] diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index bfa8b945..cf02adec 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -495,6 +495,9 @@ public void TestCancelParsing(ProviderKind kind) 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" }); @@ -502,6 +505,7 @@ public void TestCancelParsing(ProviderKind kind) Assert.IsTrue(parser.HelpRequested); Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.LastException); + Assert.IsTrue(new[] { "-Argument2", "bar" }.AsSpan().SequenceEqual(parser.ParseResult.RemainingArguments.Span)); Assert.AreEqual("DoesCancel", parser.ParseResult.ArgumentName); Assert.IsTrue(parser.GetArgument("Argument1").HasValue); Assert.AreEqual("foo", (string)parser.GetArgument("Argument1").Value); @@ -527,6 +531,7 @@ static void handler1(object sender, ArgumentParsedEventArgs e) Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.LastException); Assert.AreEqual("DoesNotCancel", parser.ParseResult.ArgumentName); + Assert.IsTrue(new[] { "-Argument2", "bar" }.AsSpan().SequenceEqual(parser.ParseResult.RemainingArguments.Span)); Assert.IsFalse(parser.HelpRequested); Assert.IsTrue(parser.GetArgument("Argument1").HasValue); Assert.AreEqual("foo", (string)parser.GetArgument("Argument1").Value); @@ -552,6 +557,7 @@ static void handler2(object sender, ArgumentParsedEventArgs e) 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); @@ -564,10 +570,42 @@ static void handler2(object sender, ArgumentParsedEventArgs e) 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); } + [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); + Assert.IsTrue(new[] { "-Argument2", "bar" }.AsSpan().SequenceEqual(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); + } + [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] public void TestParseOptionsAttribute(ProviderKind kind) @@ -1257,6 +1295,7 @@ private static void TestParse(CommandLineParser target, string co 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); diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 74c5e2d0..b7309bca 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -901,7 +901,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// Parses the specified command line arguments. /// /// The command line arguments. - public object? Parse(ReadOnlySpan args) + public object? Parse(ReadOnlyMemory args) { try { @@ -940,7 +940,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = throw new ArgumentOutOfRangeException(nameof(index)); } - return Parse(args.AsSpan(index)); + return Parse(args.AsMemory(index)); } /// @@ -1000,7 +1000,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = throw new ArgumentOutOfRangeException(nameof(index)); } - return ParseWithErrorHandling(args.AsSpan(index)); + return ParseWithErrorHandling(args.AsMemory(index)); } /// @@ -1009,7 +1009,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// required. /// /// The command line arguments. - public object? ParseWithErrorHandling(ReadOnlySpan args) + public object? ParseWithErrorHandling(ReadOnlyMemory args) { EventHandler? handler = null; if (_parseOptions.DuplicateArguments == ErrorMode.Warning) @@ -1493,7 +1493,7 @@ private void VerifyPositionalArgumentRules() } } - private object? ParseCore(ReadOnlySpan args) + private object? ParseCore(ReadOnlyMemory args) { // Reset all arguments to their default value. foreach (CommandLineArgument argument in _arguments) @@ -1505,15 +1505,17 @@ private void VerifyPositionalArgumentRules() int positionalArgumentIndex = 0; var cancelParsing = CancelMode.None; - for (int x = 0; x < args.Length; ++x) + CommandLineArgument? lastArgument = null; + int x; + for (x = 0; x < args.Length; ++x) { - string arg = args[x]; + string arg = args.Span[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. - (cancelParsing, x) = ParseNamedArgument(args, x, argumentNamePrefix.Value); + (cancelParsing, x, lastArgument) = ParseNamedArgument(args.Span, x, argumentNamePrefix.Value); if (cancelParsing != CancelMode.None) { break; @@ -1538,7 +1540,8 @@ private void VerifyPositionalArgumentRules() // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler // or the CancelParsing property. - cancelParsing = ParseArgumentValue(_arguments[positionalArgumentIndex], arg, arg.AsMemory()); + lastArgument = _arguments[positionalArgumentIndex]; + cancelParsing = ParseArgumentValue(lastArgument, arg, arg.AsMemory()); if (cancelParsing != CancelMode.None) { break; @@ -1548,6 +1551,7 @@ private void VerifyPositionalArgumentRules() if (cancelParsing == CancelMode.Abort) { + ParseResult = ParseResult.FromCanceled(lastArgument!.ArgumentName, args.Slice(x + 1)); return null; } @@ -1590,9 +1594,12 @@ private void VerifyPositionalArgumentRules() argument.ApplyPropertyValue(commandLineArguments); } - // Reset in case it was set by a method argument that didn't cancel parsing. + ParseResult = cancelParsing == CancelMode.None + ? ParseResult.FromSuccess() + : ParseResult.FromSuccess(lastArgument!.ArgumentName, args.Slice(x + 1)); + + // Reset to false in case it was set by a method argument that didn't cancel parsing. HelpRequested = false; - ParseResult = ParseResult.Success; return commandLineArguments; } @@ -1637,14 +1644,12 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri { HelpRequested = true; } - - ParseResult = ParseResult.FromCanceled(argument.ArgumentName); } return e.CancelParsing; } - private (CancelMode, int) ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo prefix) + private (CancelMode, int, CommandLineArgument?) ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo prefix) { var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitOnce(NameValueSeparator); @@ -1658,8 +1663,9 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri } else { - cancelParsing = ParseShortArgument(argumentName.Span, argumentValue); - return (cancelParsing, index); + CommandLineArgument? lastArgument; + (cancelParsing, lastArgument) = ParseShortArgument(argumentName.Span, argumentValue); + return (cancelParsing, index, lastArgument); } } @@ -1692,7 +1698,7 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri cancelParsing = ParseArgumentValue(argument, argumentValueString, argumentValueString.AsMemory()); if (cancelParsing != CancelMode.None) { - return (cancelParsing, index); + return (cancelParsing, index, argument); } if (!argument.AllowMultiValueWhiteSpaceSeparator) @@ -1703,14 +1709,14 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri if (argumentValueString != null) { - return (CancelMode.None, index); + return (CancelMode.None, index, argument); } } // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler // or the CancelParsing property. cancelParsing = ParseArgumentValue(argument, null, argumentValue); - return (cancelParsing, index); + return (cancelParsing, index, argument); } private CommandLineArgument? GetArgumentByNamePrefix(ReadOnlySpan prefix) @@ -1752,11 +1758,12 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri return foundArgument; } - private CancelMode ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? value) + private (CancelMode, CommandLineArgument?) ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory? value) { + CommandLineArgument? arg = null; foreach (var ch in name) { - var arg = GetShortArgumentOrThrow(ch); + arg = GetShortArgumentOrThrow(ch); if (!arg.IsSwitch) { throw StringProvider.CreateException(CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, name.ToString()); @@ -1765,11 +1772,11 @@ private CancelMode ParseShortArgument(ReadOnlySpan name, ReadOnlyMemory args, CommandManager manager) try { - _childCommand = (ICommand?)parser.Parse(args.Span); + _childCommand = (ICommand?)parser.Parse(args); } catch (CommandLineArgumentException) { diff --git a/src/Ookii.CommandLine/ParseResult.cs b/src/Ookii.CommandLine/ParseResult.cs index 61c40a78..ca4e235c 100644 --- a/src/Ookii.CommandLine/ParseResult.cs +++ b/src/Ookii.CommandLine/ParseResult.cs @@ -9,11 +9,13 @@ namespace Ookii.CommandLine /// public readonly struct ParseResult { - private ParseResult(ParseStatus status, CommandLineArgumentException? exception = null, string? argumentName = null) + private ParseResult(ParseStatus status, CommandLineArgumentException? exception = null, string? argumentName = null, + ReadOnlyMemory remainingArguments = default) { Status = status; LastException = exception; ArgumentName = argumentName; + RemainingArguments = remainingArguments; } /// @@ -46,12 +48,32 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception public string? ArgumentName { get; } /// - /// Gets a instance that represents successful parsing. + /// Gets any arguments that were not parsed by the if + /// parsing was canceled. /// /// - /// An instance of the structure. + /// A instance with the remaining arguments, or an empty + /// collection if there were no remaining arguments, or parsing was not canceled. /// - public static ParseResult Success => new(ParseStatus.Success); + /// + /// + /// This will always be an empty collection if parsing was not canceled. + /// + /// + public ReadOnlyMemory RemainingArguments { get; } + + /// + /// Gets a instance that represents successful parsing. + /// + /// + /// The name of the argument that canceled parsing using . + /// + /// 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. @@ -68,11 +90,12 @@ public static ParseResult FromException(CommandLineArgumentException exception) /// 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) - => new(ParseStatus.Canceled, null, argumentName ?? throw new ArgumentNullException(nameof(argumentName))); + public static ParseResult FromCanceled(string argumentName, ReadOnlyMemory remainingArguments) + => new(ParseStatus.Canceled, null, argumentName ?? throw new ArgumentNullException(nameof(argumentName)), remainingArguments); } } From 6ddbe41b5aacd08b3758f0fe9fe9e43218bfd608 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 7 Jun 2023 17:41:41 -0700 Subject: [PATCH 104/234] Remaining arguments when exception thrown. --- src/Ookii.CommandLine.Tests/CommandLineParserTest.cs | 3 ++- src/Ookii.CommandLine/CommandLineParser.cs | 12 ++++++++---- src/Ookii.CommandLine/ParseResult.cs | 5 +++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index cf02adec..7da0ab8f 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -134,7 +134,8 @@ public void ParseTestTooManyArguments(ProviderKind kind) var target = CreateParser(kind); // Only accepts one positional argument. - CheckThrows(() => target.Parse(new[] { "Foo", "Bar" }), target, CommandLineArgumentErrorCategory.TooManyArguments); + CheckThrows(() => target.Parse(new[] { "Foo", "Bar", "Baz" }), target, CommandLineArgumentErrorCategory.TooManyArguments); + Assert.IsTrue(new[] { "Bar", "Baz" }.AsSpan().SequenceEqual(target.ParseResult.RemainingArguments.Span)); } [TestMethod] diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index b7309bca..17811a26 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -903,15 +903,20 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// The command line arguments. public object? Parse(ReadOnlyMemory args) { + int index = -1; try { HelpRequested = false; - return ParseCore(args); + return ParseCore(args, ref index); } catch (CommandLineArgumentException ex) { HelpRequested = true; - ParseResult = ParseResult.FromException(ex); + var remaining = index < args.Length + ? args.Slice(index + 1) + : default; + + ParseResult = ParseResult.FromException(ex, remaining); throw; } } @@ -1493,7 +1498,7 @@ private void VerifyPositionalArgumentRules() } } - private object? ParseCore(ReadOnlyMemory args) + private object? ParseCore(ReadOnlyMemory args, ref int x) { // Reset all arguments to their default value. foreach (CommandLineArgument argument in _arguments) @@ -1506,7 +1511,6 @@ private void VerifyPositionalArgumentRules() var cancelParsing = CancelMode.None; CommandLineArgument? lastArgument = null; - int x; for (x = 0; x < args.Length; ++x) { string arg = args.Span[x]; diff --git a/src/Ookii.CommandLine/ParseResult.cs b/src/Ookii.CommandLine/ParseResult.cs index ca4e235c..4cfbed19 100644 --- a/src/Ookii.CommandLine/ParseResult.cs +++ b/src/Ookii.CommandLine/ParseResult.cs @@ -79,12 +79,13 @@ public static ParseResult FromSuccess(string? cancelArgumentName = null, ReadOnl /// 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) - => new(ParseStatus.Error, exception ?? throw new ArgumentNullException(nameof(exception)), exception.ArgumentName); + 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. From 7bcb3500c3dc359a70321a258289e5a0bdc74b6c Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 8 Jun 2023 12:48:04 -0700 Subject: [PATCH 105/234] More extensive testing for RemainingArguments. --- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 3 + .../CommandLineParserTest.cs | 141 +++++++++++------- src/Ookii.CommandLine/CommandLineParser.cs | 8 +- src/Ookii.CommandLine/ParseResult.cs | 19 ++- 4 files changed, 103 insertions(+), 68 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index b98ae762..0469a2c7 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -105,6 +105,9 @@ partial class ThrowingArguments { private int _throwingArgument; + [CommandLineArgument(Position = 0)] + public string Arg { get; set; } + [CommandLineArgument] public int ThrowingArgument { diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 7da0ab8f..5c51e481 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -113,9 +113,9 @@ public void ParseTest(ProviderKind kind) // 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"); + CheckThrows(target, new[] { "val1", "2", "--arg6", "val6" }, CommandLineArgumentErrorCategory.UnknownArgument, "-arg6", remainingArgumentCount: 2); // Short name cannot be used - CheckThrows(() => target.Parse(new[] { "val1", "2", "-arg6", "val6", "-a:5.5" }), target, CommandLineArgumentErrorCategory.UnknownArgument, "a"); + CheckThrows(target, new[] { "val1", "2", "-arg6", "val6", "-a:5.5" }, CommandLineArgumentErrorCategory.UnknownArgument, "a", remainingArgumentCount: 1); } [TestMethod] @@ -124,7 +124,7 @@ public void ParseTestEmptyArguments(ProviderKind kind) { var target = CreateParser(kind); // This test was added because version 2.0 threw an IndexOutOfRangeException when you tried to specify a positional argument when there were no positional arguments defined. - CheckThrows(() => target.Parse(new[] { "Foo", "Bar" }), target, CommandLineArgumentErrorCategory.TooManyArguments); + CheckThrows(target, new[] { "Foo", "Bar" }, CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 2); } [TestMethod] @@ -134,8 +134,7 @@ public void ParseTestTooManyArguments(ProviderKind kind) var target = CreateParser(kind); // Only accepts one positional argument. - CheckThrows(() => target.Parse(new[] { "Foo", "Bar", "Baz" }), target, CommandLineArgumentErrorCategory.TooManyArguments); - Assert.IsTrue(new[] { "Bar", "Baz" }.AsSpan().SequenceEqual(target.ParseResult.RemainingArguments.Span)); + CheckThrows(target, new[] { "Foo", "Bar" }, CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 1); } [TestMethod] @@ -144,8 +143,9 @@ public void ParseTestPropertySetterThrows(ProviderKind kind) { var target = CreateParser(kind); - CheckThrows(() => target.Parse(new[] { "-ThrowingArgument", "-5" }), - target, + // No remaining arguments; exception happens after parsing finishes. + CheckThrows(target, + new[] { "-ThrowingArgument", "-5" }, CommandLineArgumentErrorCategory.ApplyValueError, "ThrowingArgument", typeof(ArgumentOutOfRangeException)); @@ -157,8 +157,8 @@ public void ParseTestConstructorThrows(ProviderKind kind) { var target = CreateParser(kind); - CheckThrows(() => target.Parse(Array.Empty()), - target, + CheckThrows(target, + Array.Empty(), CommandLineArgumentErrorCategory.CreateArgumentsTypeError, null, typeof(ArgumentException)); @@ -176,11 +176,12 @@ public void ParseTestDuplicateDictionaryKeys(ProviderKind kind) 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, + CheckThrows(target, + new[] { "-NoDuplicateKeys", "Foo=1", "-NoDuplicateKeys", "Bar=2", "-NoDuplicateKeys", "Foo=3" }, CommandLineArgumentErrorCategory.InvalidDictionaryValue, "NoDuplicateKeys", - typeof(ArgumentException)); + typeof(ArgumentException), + remainingArgumentCount: 2); } [TestMethod] @@ -205,20 +206,22 @@ public void ParseTestNameValueSeparator(ProviderKind kind) Assert.IsNotNull(args); Assert.AreEqual("test", args.Argument1); Assert.AreEqual("foo:bar", args.Argument2); - CheckThrows(() => target.Parse(new[] { "-Argument1=test" }), - target, + CheckThrows(target, + new[] { "-Argument1=test" }, CommandLineArgumentErrorCategory.UnknownArgument, - "Argument1=test"); + "Argument1=test", + remainingArgumentCount: 1); target.Options.NameValueSeparator = '='; 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.Parse(new[] { "-Argument1:test" }), - target, + CheckThrows(target, + new[] { "-Argument1:test" }, CommandLineArgumentErrorCategory.UnknownArgument, - "Argument1:test"); + "Argument1:test", + remainingArgumentCount: 1); } [TestMethod] @@ -234,19 +237,21 @@ public void ParseTestKeyValueSeparator(ProviderKind kind) 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, + CheckThrows(target, + new[] { "-CustomSeparator", "foo=bar" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "CustomSeparator", - typeof(FormatException)); + typeof(FormatException), + remainingArgumentCount: 2); // Inner exception is FormatException because what throws here is trying to convert // ">bar" to int. - CheckThrows(() => target.Parse(new[] { "-DefaultSeparator", "foo<=>bar" }), - target, + CheckThrows(target, + new[] { "-DefaultSeparator", "foo<=>bar" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "DefaultSeparator", - typeof(FormatException)); + typeof(FormatException), + remainingArgumentCount: 2); } [TestMethod] @@ -711,16 +716,16 @@ public void TestLongShortMode(ProviderKind kind) Assert.AreEqual(5, result.Arg2); // Combining non-switches is an error. - CheckThrows(() => parser.Parse(new[] { "-sf" }), parser, CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, "sf"); + CheckThrows(parser, new[] { "-sf" }, CommandLineArgumentErrorCategory.CombinedShortNameNonSwitch, "sf", remainingArgumentCount: 1); // Can't use long argument prefix with short names. - CheckThrows(() => parser.Parse(new[] { "--s" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "s"); + CheckThrows(parser, new[] { "--s" }, CommandLineArgumentErrorCategory.UnknownArgument, "s", remainingArgumentCount: 1); // And vice versa. - CheckThrows(() => parser.Parse(new[] { "-Switch1" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "w"); + CheckThrows(parser, new[] { "-Switch1" }, CommandLineArgumentErrorCategory.UnknownArgument, "w", remainingArgumentCount: 1); // Short alias is ignored on an argument without a short name. - CheckThrows(() => parser.Parse(new[] { "-c" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "c"); + CheckThrows(parser, new[] { "-c" }, CommandLineArgumentErrorCategory.UnknownArgument, "c", remainingArgumentCount: 1); } [TestMethod] @@ -920,58 +925,59 @@ public void TestValidation(ProviderKind kind) var parser = CreateParser(kind); // Range validator on property - CheckThrows(() => parser.Parse(new[] { "-Arg1", "0" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg1"); + CheckThrows(parser, new[] { "-Arg1", "0" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg1", remainingArgumentCount: 2); 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"); + CheckThrows(parser, new[] { "-Arg1", "6" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg1", remainingArgumentCount: 2); // Not null or empty on ctor parameter - CheckThrows(() => parser.Parse(new[] { "" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "arg2"); + CheckThrows(parser, new[] { "" }, CommandLineArgumentErrorCategory.ValidationFailed, "arg2", remainingArgumentCount: 1); result = parser.Parse(new[] { " " }); Assert.AreEqual(" ", result.Arg2); // Multiple validators on method - CheckThrows(() => parser.Parse(new[] { "-Arg3", "1238" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3"); + CheckThrows(parser, new[] { "-Arg3", "1238" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3", remainingArgumentCount: 2); Assert.AreEqual(0, ValidationArguments.Arg3Value); - CheckThrows(() => parser.Parse(new[] { "-Arg3", "123" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3"); + CheckThrows(parser, new[] { "-Arg3", "123" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3", remainingArgumentCount: 2); Assert.AreEqual(0, ValidationArguments.Arg3Value); - CheckThrows(() => parser.Parse(new[] { "-Arg3", "7001" }), parser, CommandLineArgumentErrorCategory.ValidationFailed, "Arg3"); + 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); 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"); + 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 = 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"); + // 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 = 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)); + CheckThrows(parser, new[] { "-Day", "foo" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(FormatException), remainingArgumentCount: 2); + CheckThrows(parser, new[] { "-Day", "9" }, CommandLineArgumentErrorCategory.ValidationFailed, "Day", remainingArgumentCount: 2); + CheckThrows(parser, new[] { "-Day", "" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(FormatException), remainingArgumentCount: 2); 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"); + CheckThrows(parser, new[] { "-Day2", "foo" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day2", typeof(FormatException), remainingArgumentCount: 2); + CheckThrows(parser, new[] { "-Day2", "9" }, CommandLineArgumentErrorCategory.ValidationFailed, "Day2", remainingArgumentCount: 2); 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"); + CheckThrows(parser, new[] { "-NotNull", "" }, CommandLineArgumentErrorCategory.ValidationFailed, "NotNull", remainingArgumentCount: 2); } [TestMethod] @@ -980,15 +986,16 @@ public void TestRequires(ProviderKind kind) { var parser = CreateParser(kind); + // None of these have remaining arguments because validation happens after parsing. 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"); + CheckThrows(parser, new[] { "-Port", "9000" }, 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"); + 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 = parser.Parse(new[] { "-Protocol", "1", "-Address", "127.0.0.1", "-Throughput", "10" }); Assert.AreEqual(IPAddress.Loopback, result.Address); Assert.AreEqual(10, result.Throughput); @@ -1003,7 +1010,8 @@ public void TestProhibits(ProviderKind kind) 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"); + // No remaining arguments because validation happens after parsing. + CheckThrows(parser, new[] { "-Path", "test", "-Address", "127.0.0.1" }, CommandLineArgumentErrorCategory.DependencyFailed, "Path"); } [TestMethod] @@ -1013,7 +1021,7 @@ public void TestRequiresAny(ProviderKind kind) var parser = CreateParser(kind); // No need to check if the arguments work indivially since TestRequires and TestProhibits already did that. - CheckThrows(() => parser.Parse(Array.Empty()), parser, CommandLineArgumentErrorCategory.MissingRequiredArgument); + CheckThrows(parser, Array.Empty(), CommandLineArgumentErrorCategory.MissingRequiredArgument); } [TestMethod] @@ -1072,10 +1080,10 @@ public void TestMultiValueWhiteSpaceSeparator(ProviderKind kind) 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", typeof(FormatException)); + 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.Parse(new[] { "1", "-Multi:2", "2", "3", "4", "-Other", "5", "6" }), parser, CommandLineArgumentErrorCategory.TooManyArguments); + CheckThrows(parser, new[] { "1", "-Multi:2", "2", "3", "4", "-Other", "5", "6" }, CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 5); } [TestMethod] @@ -1101,7 +1109,7 @@ public void TestInjection(ProviderKind kind) public void TestDuplicateArguments(ProviderKind kind) { var parser = CreateParser(kind); - CheckThrows(() => parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }), parser, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1"); + CheckThrows(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1", remainingArgumentCount: 2); parser.Options.DuplicateArguments = ErrorMode.Allow; var result = parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }); Assert.AreEqual("bar", result.Argument1); @@ -1124,7 +1132,7 @@ public void TestDuplicateArguments(ProviderKind kind) // 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"); + CheckThrows(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1", remainingArgumentCount: 2); Assert.IsFalse(handlerCalled); // Now it is called. @@ -1210,10 +1218,10 @@ public void TestAutoPrefixAliases(ProviderKind kind) Assert.IsTrue(result.EnablePrefix); // Ambiguous prefix - CheckThrows(() => parser.Parse(new[] { "-p", "foo" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "p"); + CheckThrows(parser, new[] { "-p", "foo" }, CommandLineArgumentErrorCategory.UnknownArgument, "p", remainingArgumentCount: 2); // Ambiguous due to alias. - CheckThrows(() => parser.Parse(new[] { "-pr", "foo" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "pr"); + CheckThrows(parser, new[] { "-pr", "foo" }, CommandLineArgumentErrorCategory.UnknownArgument, "pr", remainingArgumentCount: 2); // Prefix of an alias. result = parser.Parse(new[] { "-pre" }); @@ -1223,7 +1231,7 @@ public void TestAutoPrefixAliases(ProviderKind kind) // Disable auto prefix aliases. var options = new ParseOptions() { AutoPrefixAliases = false }; parser = CreateParser(kind, options); - CheckThrows(() => parser.Parse(new[] { "-pro", "foo", "-Po", "5", "-e" }), parser, CommandLineArgumentErrorCategory.UnknownArgument, "pro"); + CheckThrows(parser, new[] { "-pro", "foo", "-Po", "5", "-e" }, CommandLineArgumentErrorCategory.UnknownArgument, "pro", remainingArgumentCount: 5); } private class ExpectedArgument @@ -1365,6 +1373,14 @@ private static void CheckThrows(Action operation, CommandLineParser parser, Comm } } + private static void CheckThrows(CommandLineParser parser, string[] arguments, CommandLineArgumentErrorCategory category, string argumentName = null, Type innerExceptionType = null, int remainingArgumentCount = 0) + { + CheckThrows(() => parser.Parse(arguments), parser, category, argumentName, innerExceptionType); + var remaining = arguments.AsMemory(arguments.Length - remainingArgumentCount); + AssertSpanEqual(remaining.Span, parser.ParseResult.RemainingArguments.Span); + } + + internal static CommandLineParser CreateParser(ProviderKind kind, ParseOptions options = null) #if NET7_0_OR_GREATER where T : class, IParserProvider @@ -1417,5 +1433,14 @@ public static IEnumerable ProviderKinds 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())} }}"); + } + } } } diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 17811a26..0b7ace02 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -912,11 +912,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = catch (CommandLineArgumentException ex) { HelpRequested = true; - var remaining = index < args.Length - ? args.Slice(index + 1) - : default; - - ParseResult = ParseResult.FromException(ex, remaining); + ParseResult = ParseResult.FromException(ex, args.Slice(index)); throw; } } @@ -1542,8 +1538,6 @@ private void VerifyPositionalArgumentRules() throw StringProvider.CreateException(CommandLineArgumentErrorCategory.TooManyArguments); } - // ParseArgumentValue returns true if parsing was canceled by the ArgumentParsed event handler - // or the CancelParsing property. lastArgument = _arguments[positionalArgumentIndex]; cancelParsing = ParseArgumentValue(lastArgument, arg, arg.AsMemory()); if (cancelParsing != CancelMode.None) diff --git a/src/Ookii.CommandLine/ParseResult.cs b/src/Ookii.CommandLine/ParseResult.cs index 4cfbed19..af6fea08 100644 --- a/src/Ookii.CommandLine/ParseResult.cs +++ b/src/Ookii.CommandLine/ParseResult.cs @@ -49,15 +49,28 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception /// /// Gets any arguments that were not parsed by the if - /// parsing was canceled. + /// parsing was canceled or an error occurred. /// /// /// A instance with the remaining arguments, or an empty - /// collection if there were no remaining arguments, or parsing was not canceled. + /// collection if there were no remaining arguments. /// /// /// - /// This will always be an empty collection if parsing was not canceled. + /// 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; } From 4ab384d3aeb79647b77f81b7d9b4a63893cece3f Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 8 Jun 2023 12:50:52 -0700 Subject: [PATCH 106/234] CheckThrows cleanup. --- .../CommandLineParserTest.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 5c51e481..cd551bf4 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -511,7 +511,7 @@ public void TestCancelParsing(ProviderKind kind) Assert.IsTrue(parser.HelpRequested); Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.LastException); - Assert.IsTrue(new[] { "-Argument2", "bar" }.AsSpan().SequenceEqual(parser.ParseResult.RemainingArguments.Span)); + 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); @@ -537,7 +537,7 @@ static void handler1(object sender, ArgumentParsedEventArgs e) Assert.AreEqual(ParseStatus.Canceled, parser.ParseResult.Status); Assert.IsNull(parser.ParseResult.LastException); Assert.AreEqual("DoesNotCancel", parser.ParseResult.ArgumentName); - Assert.IsTrue(new[] { "-Argument2", "bar" }.AsSpan().SequenceEqual(parser.ParseResult.RemainingArguments.Span)); + 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); @@ -589,7 +589,7 @@ public void TestCancelParsingSuccess(ProviderKind kind) var result = parser.Parse(new[] { "-Argument1", "foo", "-DoesCancelWithSuccess", "-Argument2", "bar" }); Assert.AreEqual(ParseStatus.Success, parser.ParseResult.Status); Assert.AreEqual("DoesCancelWithSuccess", parser.ParseResult.ArgumentName); - Assert.IsTrue(new[] { "-Argument2", "bar" }.AsSpan().SequenceEqual(parser.ParseResult.RemainingArguments.Span)); + AssertSpanEqual(new[] { "-Argument2", "bar" }.AsSpan(), parser.ParseResult.RemainingArguments.Span); Assert.IsNotNull(result); Assert.IsFalse(parser.HelpRequested); Assert.IsFalse(result.DoesNotCancel); @@ -1348,10 +1348,14 @@ private static void TestParse(CommandLineParser target, string co } private static void CheckThrows(Action operation, CommandLineParser parser, CommandLineArgumentErrorCategory category, string argumentName = null, Type innerExceptionType = null) + { + } + + private static void CheckThrows(CommandLineParser parser, string[] arguments, CommandLineArgumentErrorCategory category, string argumentName = null, Type innerExceptionType = null, int remainingArgumentCount = 0) { try { - operation(); + parser.Parse(arguments); Assert.Fail("Expected CommandLineException was not thrown."); } catch (CommandLineArgumentException ex) @@ -1370,17 +1374,12 @@ private static void CheckThrows(Action operation, CommandLineParser parser, Comm { Assert.IsInstanceOfType(ex.InnerException, innerExceptionType); } - } - } - private static void CheckThrows(CommandLineParser parser, string[] arguments, CommandLineArgumentErrorCategory category, string argumentName = null, Type innerExceptionType = null, int remainingArgumentCount = 0) - { - CheckThrows(() => parser.Parse(arguments), parser, category, argumentName, innerExceptionType); - var remaining = arguments.AsMemory(arguments.Length - remainingArgumentCount); - AssertSpanEqual(remaining.Span, parser.ParseResult.RemainingArguments.Span); + var remaining = arguments.AsMemory(arguments.Length - remainingArgumentCount); + AssertSpanEqual(remaining.Span, parser.ParseResult.RemainingArguments.Span); + } } - internal static CommandLineParser CreateParser(ProviderKind kind, ParseOptions options = null) #if NET7_0_OR_GREATER where T : class, IParserProvider From 230777fc6ca199ebd27eb859a74fd7d601e312ce Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 8 Jun 2023 14:40:50 -0700 Subject: [PATCH 107/234] Allow CancelMode as a return type for method arguments. --- .../ParserGenerator.cs | 30 +++++--- src/Ookii.CommandLine.Generator/TypeHelper.cs | 2 + src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 21 ++++++ .../CommandLineParserTest.cs | 74 +++++++++++++------ src/Ookii.CommandLine/CommandLineArgument.cs | 13 ++-- .../CommandLineArgumentAttribute.cs | 31 ++++---- .../Support/GeneratedArgument.cs | 8 +- .../Support/ReflectionArgument.cs | 17 ++--- 8 files changed, 127 insertions(+), 69 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 514a3acb..340a0adb 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -14,12 +14,19 @@ 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 bool HasBooleanReturn { get; set; } + public ReturnType ReturnType { get; set; } } private struct PositionalArgumentInfo @@ -499,15 +506,14 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> arguments = "parser"; } - if (info.HasBooleanReturn) + var methodCall = info.ReturnType switch { - _builder.AppendArgument($"callMethod: (value, parser) => {_argumentsClass.ToDisplayString()}.{member.Name}({arguments})"); - } - else - { - _builder.AppendArgument($"callMethod: (value, parser) => {{ {_argumentsClass.ToDisplayString()}.{member.Name}({arguments}); return true; }}"); - } + ReturnType.CancelMode => $"callMethod: (value, parser) => {_argumentsClass.ToDisplayString()}.{member.Name}({arguments})", + ReturnType.Boolean => $"callMethod: (value, parser) => {_argumentsClass.ToDisplayString()}.{member.Name}({arguments}) ? Ookii.CommandLine.CancelMode.None : Ookii.CommandLine.CancelMode.Abort", + _ => $"callMethod: (value, parser) => {{ {_argumentsClass.ToDisplayString()}.{member.Name}({arguments}); return Ookii.CommandLine.CancelMode.None; }}" + }; + _builder.AppendArgument(methodCall); if (argumentInfo.DefaultValue != null) { _context.ReportDiagnostic(Diagnostics.DefaultValueWithMethod(member)); @@ -720,9 +726,13 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> } var info = new MethodArgumentInfo(); - if (method.ReturnType.SpecialType == SpecialType.System_Boolean) + if (method.ReturnType.SymbolEquals(_typeHelper.CancelMode)) + { + info.ReturnType = ReturnType.CancelMode; + } + else if (method.ReturnType.SpecialType == SpecialType.System_Boolean) { - info.HasBooleanReturn = true; + info.ReturnType = ReturnType.Boolean; } else if (method.ReturnType.SpecialType != SpecialType.System_Void) { diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index 83372994..51c7a6fa 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -63,6 +63,8 @@ public TypeHelper(Compilation compilation) 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"); diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 0469a2c7..aaf4d349 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -271,6 +271,27 @@ public static bool Cancel() return false; } + [CommandLineArgument] + public static CancelMode CancelModeAbort() + { + CalledMethodName = nameof(CancelModeAbort); + return CancelMode.Abort; + } + + [CommandLineArgument] + public static CancelMode CancelModeSuccess() + { + CalledMethodName = nameof(CancelModeSuccess); + return CancelMode.Success; + } + + [CommandLineArgument] + public static CancelMode CancelModeNone() + { + CalledMethodName = nameof(CancelModeNone); + return CancelMode.None; + } + [CommandLineArgument] public static bool CancelWithHelp(CommandLineParser parser) { diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index cd551bf4..0a5c9713 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Net; @@ -739,46 +740,46 @@ public void TestMethodArguments(ProviderKind kind) Assert.IsNull(parser.GetArgument("NotStatic")); Assert.IsNull(parser.GetArgument("NotPublic")); - Assert.IsNotNull(parser.Parse(new[] { "-NoCancel" })); - Assert.IsFalse(parser.HelpRequested); + CheckSuccess(parser, new[] { "-NoCancel" }); Assert.AreEqual(nameof(MethodArguments.NoCancel), MethodArguments.CalledMethodName); - Assert.IsNull(parser.Parse(new[] { "-Cancel" })); - Assert.IsFalse(parser.HelpRequested); + CheckCanceled(parser, new[] { "-Cancel", "Foo" }, "Cancel", false, 1); Assert.AreEqual(nameof(MethodArguments.Cancel), MethodArguments.CalledMethodName); - Assert.IsNull(parser.Parse(new[] { "-CancelWithHelp" })); - Assert.IsTrue(parser.HelpRequested); + CheckCanceled(parser, new[] { "-CancelWithHelp" }, "CancelWithHelp", true, 0); Assert.AreEqual(nameof(MethodArguments.CancelWithHelp), MethodArguments.CalledMethodName); - Assert.IsNotNull(parser.Parse(new[] { "-CancelWithValue", "1" })); - Assert.IsFalse(parser.HelpRequested); + CheckSuccess(parser, new[] { "-CancelWithValue", "1" }); Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); Assert.AreEqual(1, MethodArguments.Value); - Assert.IsNull(parser.Parse(new[] { "-CancelWithValue", "-1" })); - Assert.IsFalse(parser.HelpRequested); + CheckCanceled(parser, new[] { "-CancelWithValue", "-1" }, "CancelWithValue", false); Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); Assert.AreEqual(-1, MethodArguments.Value); - Assert.IsNotNull(parser.Parse(new[] { "-CancelWithValueAndHelp", "1" })); - Assert.IsFalse(parser.HelpRequested); + CheckSuccess(parser, new[] { "-CancelWithValueAndHelp", "1" }); Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); Assert.AreEqual(1, MethodArguments.Value); - Assert.IsNull(parser.Parse(new[] { "-CancelWithValueAndHelp", "-1" })); - Assert.IsTrue(parser.HelpRequested); + CheckCanceled(parser, new[] { "-CancelWithValueAndHelp", "-1", "bar" }, "CancelWithValueAndHelp", true, 1); Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); Assert.AreEqual(-1, MethodArguments.Value); - Assert.IsNotNull(parser.Parse(new[] { "-NoReturn" })); - Assert.IsFalse(parser.HelpRequested); + CheckSuccess(parser, new[] { "-NoReturn" }); Assert.AreEqual(nameof(MethodArguments.NoReturn), MethodArguments.CalledMethodName); - Assert.IsNotNull(parser.Parse(new[] { "42" })); - Assert.IsFalse(parser.HelpRequested); + CheckSuccess(parser, new[] { "42" }); Assert.AreEqual(nameof(MethodArguments.Positional), MethodArguments.CalledMethodName); Assert.AreEqual(42, MethodArguments.Value); + + CheckCanceled(parser, new[] { "-CancelModeAbort", "Foo" }, "CancelModeAbort", false, 1); + Assert.AreEqual(nameof(MethodArguments.CancelModeAbort), MethodArguments.CalledMethodName); + + CheckSuccess(parser, new[] { "-CancelModeSuccess", "Foo" }, "CancelModeSuccess", 1); + Assert.AreEqual(nameof(MethodArguments.CancelModeSuccess), MethodArguments.CalledMethodName); + + CheckSuccess(parser, new[] { "-CancelModeNone" }); + Assert.AreEqual(nameof(MethodArguments.CancelModeNone), MethodArguments.CalledMethodName); } [TestMethod] @@ -1347,10 +1348,6 @@ private static void TestParse(CommandLineParser target, string co } } - private static void CheckThrows(Action operation, CommandLineParser parser, CommandLineArgumentErrorCategory category, string argumentName = null, Type innerExceptionType = null) - { - } - private static void CheckThrows(CommandLineParser parser, string[] arguments, CommandLineArgumentErrorCategory category, string argumentName = null, Type innerExceptionType = null, int remainingArgumentCount = 0) { try @@ -1376,10 +1373,35 @@ private static void CheckThrows(CommandLineParser parser, string[] arguments, Co } var remaining = arguments.AsMemory(arguments.Length - remainingArgumentCount); - AssertSpanEqual(remaining.Span, parser.ParseResult.RemainingArguments.Span); + 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 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 @@ -1441,5 +1463,11 @@ public static void AssertSpanEqual(ReadOnlySpan expected, ReadOnlySpan 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/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 6fa9bed4..d548687e 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -162,8 +162,7 @@ public CancelMode SetValue(CommandLineArgument argument, object? value) Value = value; try { - // TODO: Support methods returning CancelMode. - return argument.CallMethod(value) ? CancelMode.None : CancelMode.Abort; + return argument.CallMethod(value); } catch (TargetInvocationException ex) { @@ -224,7 +223,7 @@ private static ArgumentInfo CreateInfo(CommandLineParser parser, string argument return info; } - protected override bool CallMethod(object? value) => true; + protected override CancelMode CallMethod(object? value) => CancelMode.Abort; protected override object? GetProperty(object target) => throw new InvalidOperationException(); protected override void SetProperty(object target, object? value) => throw new InvalidOperationException(); } @@ -256,7 +255,7 @@ private static ArgumentInfo CreateInfo(CommandLineParser parser, string argument }; } - protected override bool CallMethod(object? value) => AutomaticVersion(Parser); + protected override CancelMode CallMethod(object? value) => AutomaticVersion(Parser); protected override object? GetProperty(object target) => throw new InvalidOperationException(); protected override void SetProperty(object target, object? value) => throw new InvalidOperationException(); } @@ -1165,7 +1164,7 @@ public override string ToString() /// /// This argument does not use a method. /// - protected abstract bool CallMethod(object? value); + protected abstract CancelMode CallMethod(object? value); /// /// Determines the value description if one wasn't explicitly given. @@ -1562,12 +1561,12 @@ private IValueHelper CreateValueHelper() }); } - private static bool AutomaticVersion(CommandLineParser parser) + private static CancelMode AutomaticVersion(CommandLineParser parser) { ShowVersion(parser.StringProvider, parser.ArgumentsType.Assembly, parser.ApplicationFriendlyName); // Cancel parsing but do not show help. - return false; + return CancelMode.Abort; } internal static string DetermineArgumentName(string? explicitName, string memberName, NameTransform? transform) diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index 07c1f4a5..506cd860 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -22,14 +22,10 @@ namespace Ookii.CommandLine; /// signatures: /// /// -/// 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(); +/// 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 @@ -38,17 +34,22 @@ namespace Ookii.CommandLine; /// /// /// 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 . +/// command line is complete. /// /// -/// Unlike using the or -/// event, canceling parsing with the return value does not automatically print the usage -/// help when using the method, the +/// The return type must be either , or . +/// Using is equivalent to returning , and when +/// using , returning is equivalent to returning +/// . +/// +/// +/// Unlike using the property 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 . +/// property to in the +/// target method. /// /// /// diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 15d55d94..835c9aa7 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -18,12 +18,12 @@ public class GeneratedArgument : CommandLineArgument { private readonly Action? _setProperty; private readonly Func? _getProperty; - private readonly Func? _callMethod; + 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) + Func? callMethod, string defaultValueDescription, string? defaultKeyDescription) : base(info) { _setProperty = setProperty; _getProperty = getProperty; @@ -87,7 +87,7 @@ public static GeneratedArgument Create(CommandLineParser parser, IEnumerable? validationAttributes = null, Action? setProperty = null, Func? getProperty = null, - Func? callMethod = null) + Func? callMethod = null) { var info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, memberName, attribute, multiValueSeparatorAttribute, descriptionAttribute, valueDescriptionAttribute, allowDuplicateDictionaryKeys, @@ -112,7 +112,7 @@ public static GeneratedArgument Create(CommandLineParser parser, protected override bool CanSetProperty => _setProperty != null; /// - protected override bool CallMethod(object? value) + protected override CancelMode CallMethod(object? value) { if (_callMethod == null) { diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index ab3ad9a7..0a64c00a 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -61,7 +61,7 @@ protected override void SetProperty(object target, object? value) return _property.GetValue(target); } - protected override bool CallMethod(object? value) + protected override CancelMode CallMethod(object? value) { if (_method is not MethodArgumentInfo info) { @@ -82,15 +82,12 @@ protected override bool CallMethod(object? value) parameters[index] = Parser; } - var returnValue = info.Method.Invoke(null, parameters); - if (returnValue == null) + return info.Method.Invoke(null, parameters) switch { - return true; - } - else - { - return (bool)returnValue; - } + CancelMode mode => mode, + false => CancelMode.Abort, + _ => CancelMode.None + }; } internal static CommandLineArgument Create(CommandLineParser parser, PropertyInfo property) @@ -371,7 +368,7 @@ private static (MethodArgumentInfo, Type, bool)? DetermineMethodArgumentInfo(Met { var parameters = method.GetParameters(); if (!method.IsStatic || - method.ReturnType != typeof(bool) && method.ReturnType != typeof(void) || + (method.ReturnType != typeof(bool) && method.ReturnType != typeof(void) && method.ReturnType != typeof(CancelMode)) || parameters.Length > 2) { return null; From 7046c64f3d2c3a5f172c8f1c63aa3a6e732ccce2 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 8 Jun 2023 15:33:14 -0700 Subject: [PATCH 108/234] Include global namespace in generated code. --- .../CommandGenerator.cs | 4 +- .../ConverterGenerator.cs | 6 +-- src/Ookii.CommandLine.Generator/Extensions.cs | 13 ++++++ .../ParserGenerator.cs | 42 +++++++++---------- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index ec2a2017..924244c1 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -81,7 +81,7 @@ public void Generate() var source = GenerateManager(manager); if (source != null) { - _context.AddSource(manager.ToDisplayString().ToIdentifier(".g.cs"), SourceText.From(source, Encoding.UTF8)); + _context.AddSource(manager.ToQualifiedName().ToIdentifier(".g.cs"), SourceText.From(source, Encoding.UTF8)); } } } @@ -168,7 +168,7 @@ public void Generate() private bool GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType, ArgumentsClassAttributes? commandAttributes) { var useCustomParsing = commandType.ImplementsInterface(_typeHelper.ICommandWithCustomParsing); - var commandTypeName = commandType.ToDisplayString(); + var commandTypeName = commandType.ToQualifiedName(); if (useCustomParsing) { builder.AppendLine($"yield return new Ookii.CommandLine.Support.GeneratedCommandInfoWithCustomParsing<{commandTypeName}>("); diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index b380a3ea..f019d4c2 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -67,7 +67,7 @@ public ConverterGenerator(TypeHelper typeHelper, SourceProductionContext context return null; } - info.Name = GenerateName(type.ToDisplayString()); + info.Name = GenerateName(type.ToQualifiedName()); _converters.Add(type, info); converter = info; } @@ -189,11 +189,11 @@ private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, Con builder.OpenBlock(); if (info.ParseMethod) { - builder.AppendLine($"return {type.ToDisplayString()}.Parse(value{culture});"); + builder.AppendLine($"return {type.ToQualifiedName()}.Parse(value{culture});"); } else { - builder.AppendLine($"return new {type.ToDisplayString()}(value);"); + builder.AppendLine($"return new {type.ToQualifiedName()}(value);"); } builder.CloseBlock(); // try diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index a690f4f0..cf1a6328 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -9,6 +9,14 @@ namespace Ookii.CommandLine.Generator; internal static class Extensions { + // This is the format used to emit type names in the output. It includes the global namespace + // so that this doesn't break if the class matches the namespace name. + private static readonly SymbolDisplayFormat QualifiedFormat = new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers); + public static bool DerivesFrom(this ITypeSymbol symbol, ITypeSymbol? baseClass) { if (baseClass == null) @@ -214,4 +222,9 @@ public static bool CheckType(this AttributeData data, ITypeSymbol? attributeType public static Location? GetLocation(this AttributeData attribute) => attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span); + + public static string ToQualifiedName(this ITypeSymbol symbol) + { + return symbol.ToDisplayString(QualifiedFormat); + } } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 340a0adb..f4cb5802 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -141,11 +141,11 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen { // We cannot rely on default interface implementations, because that makes the methods // uncallable without a generic type argument. - _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling();"); + _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling();"); _builder.AppendLine(); - _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); + _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); _builder.AppendLine(); - _builder.AppendLine($"public static {nullableType.ToDisplayString()} Parse(string[] args, int index, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args, index);"); + _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(string[] args, int index, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args, index);"); _builder.CloseBlock(); // class } @@ -366,7 +366,7 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> ? "null" : $"keyValueSeparatorAttribute{member.Name}.Separator"; - converter = $"new Ookii.CommandLine.Conversion.KeyValuePairConverter<{keyType.ToDisplayString()}, {rawValueType.ToDisplayString()}>({keyConverter}, {valueConverter}, {separator}, {allowsNull.ToCSharpString()})"; + converter = $"new Ookii.CommandLine.Conversion.KeyValuePairConverter<{keyType.ToQualifiedName()}, {rawValueType.ToQualifiedName()}>({keyConverter}, {valueConverter}, {separator}, {allowsNull.ToCSharpString()})"; } } else if (collectionType != null) @@ -389,7 +389,7 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> { isRequired = true; requiredProperties ??= new(); - requiredProperties.Add((member.Name, property.Type.ToDisplayString(), notNullAnnotation)); + requiredProperties.Add((member.Name, property.Type.ToQualifiedName(), notNullAnnotation)); } } else @@ -409,9 +409,9 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> _builder.AppendLine($"yield return Ookii.CommandLine.Support.GeneratedArgument.Create("); _builder.IncreaseIndent(); _builder.AppendArgument("parser"); - _builder.AppendArgument($"argumentType: typeof({argumentType.ToDisplayString()})"); - _builder.AppendArgument($"elementTypeWithNullable: typeof({elementTypeWithNullable.ToDisplayString()})"); - _builder.AppendArgument($"elementType: typeof({elementType.ToDisplayString()})"); + _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()}"); @@ -420,13 +420,13 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> var valueDescriptionFormat = new SymbolDisplayFormat(genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters); if (keyType != null) { - _builder.AppendArgument($"keyType: typeof({keyType.ToDisplayString()})"); + _builder.AppendArgument($"keyType: typeof({keyType.ToQualifiedName()})"); _builder.AppendArgument($"defaultKeyDescription: \"{keyType.ToDisplayString(valueDescriptionFormat)}\""); } if (valueType != null) { - _builder.AppendArgument($"valueType: typeof({valueType.ToDisplayString()})"); + _builder.AppendArgument($"valueType: typeof({valueType.ToQualifiedName()})"); _builder.AppendArgument($"defaultValueDescription: \"{valueType.ToDisplayString(valueDescriptionFormat)}\""); } else @@ -454,10 +454,10 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> { if (property.SetMethod != null && property.SetMethod.DeclaredAccessibility == Accessibility.Public && !property.SetMethod.IsInitOnly) { - _builder.AppendArgument($"setProperty: (target, value) => (({_argumentsClass.ToDisplayString()})target).{member.Name} = ({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"); + _builder.AppendArgument($"setProperty: (target, value) => (({_argumentsClass.ToQualifiedName()})target).{member.Name} = ({originalArgumentType.ToQualifiedName()})value{notNullAnnotation}"); } - _builder.AppendArgument($"getProperty: (target) => (({_argumentsClass.ToDisplayString()})target).{member.Name}"); + _builder.AppendArgument($"getProperty: (target) => (({_argumentsClass.ToQualifiedName()})target).{member.Name}"); _builder.AppendArgument($"requiredProperty: {property.IsRequired.ToCSharpString()}"); if (argumentInfo.DefaultValue != null) { @@ -494,11 +494,11 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> { if (info.HasParserParameter) { - arguments = $"({originalArgumentType.ToDisplayString()})value{notNullAnnotation}, parser"; + arguments = $"({originalArgumentType.ToQualifiedName()})value{notNullAnnotation}, parser"; } else { - arguments = $"({originalArgumentType.ToDisplayString()})value{notNullAnnotation}"; + arguments = $"({originalArgumentType.ToQualifiedName()})value{notNullAnnotation}"; } } else if (info.HasParserParameter) @@ -508,9 +508,9 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> var methodCall = info.ReturnType switch { - ReturnType.CancelMode => $"callMethod: (value, parser) => {_argumentsClass.ToDisplayString()}.{member.Name}({arguments})", - ReturnType.Boolean => $"callMethod: (value, parser) => {_argumentsClass.ToDisplayString()}.{member.Name}({arguments}) ? Ookii.CommandLine.CancelMode.None : Ookii.CommandLine.CancelMode.Abort", - _ => $"callMethod: (value, parser) => {{ {_argumentsClass.ToDisplayString()}.{member.Name}({arguments}); return Ookii.CommandLine.CancelMode.None; }}" + 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); @@ -687,7 +687,7 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> } var converterType = (INamedTypeSymbol)argument.Value!; - return $"new {converterType.ToDisplayString()}()"; + return $"new {converterType.ToQualifiedName()}()"; } if (elementType.SpecialType == SpecialType.System_String) @@ -701,17 +701,17 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> if (elementType.TypeKind == TypeKind.Enum) { - return $"new Ookii.CommandLine.Conversion.EnumConverter(typeof({elementType.ToDisplayString()}))"; + return $"new Ookii.CommandLine.Conversion.EnumConverter(typeof({elementType.ToQualifiedName()}))"; } if (elementType.ImplementsInterface(_typeHelper.ISpanParsable?.Construct(elementType))) { - return $"new Ookii.CommandLine.Conversion.SpanParsableConverter<{elementType.ToDisplayString()}>()"; + return $"new Ookii.CommandLine.Conversion.SpanParsableConverter<{elementType.ToQualifiedName()}>()"; } if (elementType.ImplementsInterface(_typeHelper.IParsable?.Construct(elementType))) { - return $"new Ookii.CommandLine.Conversion.ParsableConverter<{elementType.ToDisplayString()}>()"; + return $"new Ookii.CommandLine.Conversion.ParsableConverter<{elementType.ToQualifiedName()}>()"; } return _converterGenerator.GetConverter(elementType); From 22abc3cb979b442788a05b9b0b9748cee7788e6e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 8 Jun 2023 16:50:27 -0700 Subject: [PATCH 109/234] Additional customization options for usage syntax. --- src/Ookii.CommandLine/UsageWriter.cs | 69 ++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index 277e5370..1a4bdf54 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -881,7 +881,7 @@ protected virtual void WriteParserUsageSyntax() SetIndent(SyntaxIndent); WriteUsageSyntaxPrefix(); - foreach (CommandLineArgument argument in Parser.Arguments) + foreach (CommandLineArgument argument in GetArgumentsInUsageOrder()) { if (argument.IsHidden) { @@ -905,10 +905,28 @@ protected virtual void WriteParserUsageSyntax() } } + WriteUsageSyntaxSuffix(); WriteLine(); // End syntax line WriteLine(); // Blank line } + /// + /// 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. + /// + /// + protected virtual IEnumerable GetArgumentsInUsageOrder() => Parser.Arguments; + /// /// Write the prefix for the usage syntax, including the executable name and, for /// subcommands, the command name. @@ -943,6 +961,27 @@ protected virtual void WriteUsageSyntaxPrefix() } } + /// + /// 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); + } + } + /// /// Writes the syntax for a single optional argument. /// @@ -1166,7 +1205,7 @@ protected virtual void WriteClassValidators() /// /// /// - /// The default implementation gets the list of arguments using the + /// The default implementation gets the list of arguments using the /// method, and calls the method for each one. /// /// @@ -1191,7 +1230,7 @@ protected virtual void WriteArgumentDescriptions() } } - var arguments = GetFilteredAndSortedArguments(); + var arguments = GetArgumentsInDescriptionOrder(); bool first = true; foreach (var argument in arguments) { @@ -1595,7 +1634,7 @@ protected virtual void WriteMoreInfoMessage() /// Arguments that are hidden are excluded from the list. /// /// - protected IEnumerable GetFilteredAndSortedArguments() + protected virtual IEnumerable GetArgumentsInDescriptionOrder() { var arguments = Parser.Arguments.Where(argument => !argument.IsHidden && ArgumentDescriptionListFilter switch { @@ -1676,7 +1715,13 @@ protected virtual void WriteCommandListUsageCore() var argumentName = transform.Apply(CommandManager.Options.StringProvider.AutomaticHelpName()); Writer.Indent = 0; - WriteCommandHelpInstruction(prefix, argumentName); + var name = ExecutableName; + if (CommandName != null) + { + name += " " + CommandName; + } + + WriteCommandHelpInstruction(name, prefix, argumentName); } } @@ -1685,8 +1730,7 @@ protected virtual void WriteCommandListUsageCore() /// /// /// - /// The base implementation calls , and adds to it - /// a string like " <command> [arguments]". + /// The base implementation calls and . /// /// /// This method is called by the base implementation of the @@ -1696,7 +1740,7 @@ protected virtual void WriteCommandListUsageCore() protected virtual void WriteCommandListUsageSyntax() { WriteUsageSyntaxPrefix(); - WriteLine(Resources.DefaultCommandUsageSuffix); + WriteUsageSyntaxSuffix(); WriteLine(); } @@ -1877,6 +1921,7 @@ protected virtual void WriteCommandDescription(string description) /// /// 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. /// @@ -1891,14 +1936,8 @@ protected virtual void WriteCommandDescription(string description) /// argument matching the automatic help argument's name. /// /// - protected virtual void WriteCommandHelpInstruction(string argumentNamePrefix, string argumentName) + protected virtual void WriteCommandHelpInstruction(string name, string argumentNamePrefix, string argumentName) { - var name = ExecutableName; - if (CommandName != null) - { - name += " " + CommandName; - } - WriteLine(Resources.CommandHelpInstructionFormat, name, argumentNamePrefix, argumentName); } From b67939ac19f63f3a9125752b039f016e9e85e038 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 8 Jun 2023 16:50:40 -0700 Subject: [PATCH 110/234] Add a sample for top-level arguments. --- src/Ookii.CommandLine.sln | 9 +- .../NestedCommands/NestedCommands.csproj | 9 +- src/Samples/README.md | 4 +- src/Samples/Subcommand/ReadCommand.cs | 18 +--- src/Samples/Subcommand/Subcommand.csproj | 2 +- src/Samples/Subcommand/WriteCommand.cs | 23 ++-- .../TopLevelArguments/CommandUsageWriter.cs | 46 ++++++++ .../TopLevelArguments/EncodingConverter.cs | 25 +++++ src/Samples/TopLevelArguments/ExitCode.cs | 10 ++ .../TopLevelArguments/GeneratedManager.cs | 9 ++ src/Samples/TopLevelArguments/Program.cs | 85 +++++++++++++++ src/Samples/TopLevelArguments/README.md | 3 + src/Samples/TopLevelArguments/ReadCommand.cs | 55 ++++++++++ .../TopLevelArguments/TopLevelArguments.cs | 32 ++++++ .../TopLevelArguments.csproj | 24 +++++ .../TopLevelArguments/TopLevelUsageWriter.cs | 35 ++++++ src/Samples/TopLevelArguments/WriteCommand.cs | 100 ++++++++++++++++++ 17 files changed, 452 insertions(+), 37 deletions(-) create mode 100644 src/Samples/TopLevelArguments/CommandUsageWriter.cs create mode 100644 src/Samples/TopLevelArguments/EncodingConverter.cs create mode 100644 src/Samples/TopLevelArguments/ExitCode.cs create mode 100644 src/Samples/TopLevelArguments/GeneratedManager.cs create mode 100644 src/Samples/TopLevelArguments/Program.cs create mode 100644 src/Samples/TopLevelArguments/README.md create mode 100644 src/Samples/TopLevelArguments/ReadCommand.cs create mode 100644 src/Samples/TopLevelArguments/TopLevelArguments.cs create mode 100644 src/Samples/TopLevelArguments/TopLevelArguments.csproj create mode 100644 src/Samples/TopLevelArguments/TopLevelUsageWriter.cs create mode 100644 src/Samples/TopLevelArguments/WriteCommand.cs diff --git a/src/Ookii.CommandLine.sln b/src/Ookii.CommandLine.sln index 80686829..719c1253 100644 --- a/src/Ookii.CommandLine.sln +++ b/src/Ookii.CommandLine.sln @@ -36,7 +36,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TrimTest", "Samples\TrimTes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ookii.CommandLine.Generator", "Ookii.CommandLine.Generator\Ookii.CommandLine.Generator.csproj", "{9C027C37-4BEA-422F-A148-1F73C6FFEF45}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ookii.CommandLine.Tests.Commands", "Ookii.CommandLine.Tests.Commands\Ookii.CommandLine.Tests.Commands.csproj", "{05AEDC31-D784-4DCA-A431-4A55323DEAFA}" +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 @@ -92,6 +94,10 @@ Global {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 @@ -105,6 +111,7 @@ Global {8717BF8D-9D9A-4DC6-8C03-B17F51D708CC} = {DC9CCD22-9B9B-4298-8C68-BC7A5A680F93} {1D7E3B10-D99E-4DF9-9AB7-5DDFF61B275D} = {DC9CCD22-9B9B-4298-8C68-BC7A5A680F93} {D3422CDA-6FAF-4EED-9CB1-814A0D519373} = {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/Samples/NestedCommands/NestedCommands.csproj b/src/Samples/NestedCommands/NestedCommands.csproj index 30fdfd20..f1ba8224 100644 --- a/src/Samples/NestedCommands/NestedCommands.csproj +++ b/src/Samples/NestedCommands/NestedCommands.csproj @@ -10,13 +10,12 @@ This is sample code, so you can use it freely. + - - - - diff --git a/src/Samples/README.md b/src/Samples/README.md index 94f282c3..c5b82d01 100644 --- a/src/Samples/README.md +++ b/src/Samples/README.md @@ -12,10 +12,12 @@ Ookii.CommandLine comes with several samples that demonstrate various aspects of arguments that require or prohibit the presence of other arguments. - The [**WPF sample**](Wpf) shows how you can use Ookii.CommandLine with a GUI application. -There are two samples demonstrating how to use subcommands: +There are three samples demonstrating how to use subcommands: - The [**subcommand sample**](Subcommand) demonstrates how to create a simple application that has multiple subcommands. - The [**nested commands sample**](NestedCommands) demonstrates how to create an application where commands can contain other commands. It also demonstrates how to create common arguments for multiple commands using a common base class. +- The [**top-level arguments sample**](TopLevelArguments) demonstrates how to use arguments that + don't belong to any subcommand before the command name. diff --git a/src/Samples/Subcommand/ReadCommand.cs b/src/Samples/Subcommand/ReadCommand.cs index 9618be57..c148d5c8 100644 --- a/src/Samples/Subcommand/ReadCommand.cs +++ b/src/Samples/Subcommand/ReadCommand.cs @@ -22,20 +22,12 @@ namespace SubcommandSample; // Check the Program.cs file to see how this command is invoked. [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 { - 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(Position = 0)] + [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 ArgumentConverter, we use a custom one provided in @@ -58,7 +50,7 @@ public override async Task RunAsync() Options = FileOptions.Asynchronous }; - using var reader = new StreamReader(_path.FullName, Encoding, true, options); + using var reader = new StreamReader(Path.FullName, Encoding, true, options); // We use a LineWrappingTextWriter to neatly wrap console output using var writer = LineWrappingTextWriter.ForConsoleOut(); diff --git a/src/Samples/Subcommand/Subcommand.csproj b/src/Samples/Subcommand/Subcommand.csproj index abf99ff3..9904827a 100644 --- a/src/Samples/Subcommand/Subcommand.csproj +++ b/src/Samples/Subcommand/Subcommand.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 disable enable Subcommand sample for Ookii.CommandLine. diff --git a/src/Samples/Subcommand/WriteCommand.cs b/src/Samples/Subcommand/WriteCommand.cs index 8f349e7e..de6c5cd1 100644 --- a/src/Samples/Subcommand/WriteCommand.cs +++ b/src/Samples/Subcommand/WriteCommand.cs @@ -11,7 +11,7 @@ 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 @@ -24,21 +24,12 @@ namespace SubcommandSample; // Check the Program.cs file to see how this command is invoked. [Command] [Description("Writes lines to a file, wrapping them to the specified width.")] -[ParseOptions(ArgumentNameTransform = NameTransform.PascalCase)] 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(Position = 0)] + [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)] @@ -71,7 +62,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, @@ -88,7 +79,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..61095f58 --- /dev/null +++ b/src/Samples/TopLevelArguments/CommandUsageWriter.cs @@ -0,0 +1,46 @@ +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; } + + public CommandUsageWriter() + { + IncludeCommandHelpInstruction = true; + } + + // 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..862d3655 --- /dev/null +++ b/src/Samples/TopLevelArguments/EncodingConverter.cs @@ -0,0 +1,25 @@ +using Ookii.CommandLine; +using Ookii.CommandLine.Conversion; +using System; +using System.Globalization; +using System.Text; + +namespace TopLevelArguments; + +// A 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..398375f3 --- /dev/null +++ b/src/Samples/TopLevelArguments/Program.cs @@ -0,0 +1,85 @@ +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() + { + // Use POSIX rules + Mode = ParsingMode.LongShort, + ArgumentNameComparison = StringComparison.InvariantCultureIgnoreCase, + ArgumentNameTransform = NameTransform.DashCase, + ValueDescriptionTransform = NameTransform.DashCase, + CommandNameComparer = StringComparer.InvariantCultureIgnoreCase, + CommandNameTransform = NameTransform.DashCase, + // The top-level arguments will have a -Version argument, so no need for a version + // command. + AutoVersionCommand = false, + UsageWriter = commandUsageWriter, + }; + + // Create a CommandManager for the commands in the current assembly. + // + // In addition to our commands, it will also have an automatic "version" command (this can + // be disabled with the options). + var manager = new GeneratedManager(commandOptions); + var parseOptions = new ParseOptions() + { + // Use POSIX rules + Mode = ParsingMode.LongShort, + ArgumentNameComparison = StringComparison.InvariantCultureIgnoreCase, + ArgumentNameTransform = NameTransform.DashCase, + ValueDescriptionTransform = NameTransform.DashCase, + // 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..af6eaaa4 --- /dev/null +++ b/src/Samples/TopLevelArguments/README.md @@ -0,0 +1,3 @@ +# Subcommands with top-level arguments sample + +TODO \ No newline at end of file diff --git a/src/Samples/TopLevelArguments/ReadCommand.cs b/src/Samples/TopLevelArguments/ReadCommand.cs new file mode 100644 index 00000000..6e2e07f6 --- /dev/null +++ b/src/Samples/TopLevelArguments/ReadCommand.cs @@ -0,0 +1,55 @@ +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 + { + var options = new FileStreamOptions() + { + Access = FileAccess.Read, + Mode = FileMode.Open, + Share = FileShare.ReadWrite | FileShare.Delete, + Options = FileOptions.Asynchronous + }; + + using var reader = new StreamReader(Program.Arguments!.Path.FullName, Program.Arguments.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 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..a3558dd2 --- /dev/null +++ b/src/Samples/TopLevelArguments/TopLevelArguments.cs @@ -0,0 +1,32 @@ +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(Position = 0)] + [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(Position = 1, 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. + [CommandLineArgument(IsShort = true)] + [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..a92af214 --- /dev/null +++ b/src/Samples/TopLevelArguments/TopLevelArguments.csproj @@ -0,0 +1,24 @@ + + + + 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..40fcc442 --- /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(Position = 0)] + [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(DefaultValue = 79, 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; } + + // 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; + } + } +} From 85604fd0d50a88db541776b0cb963e925f81d12c Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 8 Jun 2023 17:12:34 -0700 Subject: [PATCH 111/234] Remove TrimTest. --- src/Ookii.CommandLine.sln | 7 --- src/Samples/TrimTest/Program.cs | 70 ---------------------------- src/Samples/TrimTest/TrimTest.csproj | 24 ---------- 3 files changed, 101 deletions(-) delete mode 100644 src/Samples/TrimTest/Program.cs delete mode 100644 src/Samples/TrimTest/TrimTest.csproj diff --git a/src/Ookii.CommandLine.sln b/src/Ookii.CommandLine.sln index 719c1253..ff17ca06 100644 --- a/src/Ookii.CommandLine.sln +++ b/src/Ookii.CommandLine.sln @@ -32,8 +32,6 @@ 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}") = "TrimTest", "Samples\TrimTest\TrimTest.csproj", "{D3422CDA-6FAF-4EED-9CB1-814A0D519373}" -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}" @@ -82,10 +80,6 @@ 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 - {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3422CDA-6FAF-4EED-9CB1-814A0D519373}.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 @@ -110,7 +104,6 @@ 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} - {D3422CDA-6FAF-4EED-9CB1-814A0D519373} = {DC9CCD22-9B9B-4298-8C68-BC7A5A680F93} {1FF02963-CECD-4C90-8C44-68F1B1CF5988} = {DC9CCD22-9B9B-4298-8C68-BC7A5A680F93} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/Samples/TrimTest/Program.cs b/src/Samples/TrimTest/Program.cs deleted file mode 100644 index 546fa620..00000000 --- a/src/Samples/TrimTest/Program.cs +++ /dev/null @@ -1,70 +0,0 @@ -// See https://aka.ms/new-console-template for more information -using Ookii.CommandLine; -using Ookii.CommandLine.Commands; -using Ookii.CommandLine.Conversion; -using Ookii.CommandLine.Support; -using Ookii.CommandLine.Validation; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Net; - -#pragma warning disable OCL0033 - -//var manager = new TestManager(); -//return manager.RunCommand() ?? 1; - -var arguments = Arguments.Parse(); -if (arguments != null) -{ - Console.WriteLine($"Hello, World! {arguments.Test}"); -} - - -[GeneratedCommandManager] -partial class TestManager { } - -[GeneratedParser] -[ParseOptions(CaseSensitive = true, Mode = ParsingMode.LongShort)] -[Description("This is a test")] -[ApplicationFriendlyName("Trim Test")] -[RequiresAny(nameof(Test), nameof(Test2))] -partial class Arguments : ICommand -{ - [CommandLineArgument(Position = 0)] - [Description("Test argument")] - [Alias("t")] - [ValidateNotEmpty] - public string? Test { get; set; } = "Hello"; - - [CommandLineArgument(Position = 1)] - [ValueDescription("Stuff")] - [KeyValueSeparator("==")] - [MultiValueSeparator] - public Dictionary Test2 { get; set; } = default!; - - [CommandLineArgument] - public int Test3 { get; set; } - - [CommandLineArgument] - public int? Test4 { get; set; } = 5; - - [CommandLineArgument] - public FileInfo[]? File { get; set; } - - [CommandLineArgument] - public IPAddress? Ip { get; set; } - - [CommandLineArgument] - public IDictionary Arg14 { get; } = new SortedDictionary(); - - [CommandLineArgument] - public static void Foo(CommandLineParser p) - { - } - - public int Run() - { - Console.WriteLine("Hello"); - return 0; - } -} diff --git a/src/Samples/TrimTest/TrimTest.csproj b/src/Samples/TrimTest/TrimTest.csproj deleted file mode 100644 index 6a070ee7..00000000 --- a/src/Samples/TrimTest/TrimTest.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net7.0 - enable - enable - true - true - true - - - - - - - - - - - - From 176d2c80913ee7554fd60deeb0cf2ae35458c11f Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 8 Jun 2023 17:38:04 -0700 Subject: [PATCH 112/234] Warnings for ignored ParentCommand and ApplicationFriendlyName attributes. --- docs/SourceGenerationDiagnostics.md | 61 ++++++++++++++++--- .../Diagnostics.cs | 27 ++++++-- .../ParserGenerator.cs | 23 +++++-- .../Properties/Resources.Designer.cs | 36 +++++++++++ .../Properties/Resources.resx | 12 ++++ 5 files changed, 143 insertions(+), 16 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index e97d72d5..0ad7daaa 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -314,11 +314,11 @@ partial class MyCommandManager ### OCL0015 -The `ArgumentConverterAttribute` must use the `typeof` keyword. +The `ArgumentConverterAttribute` or `ParentCommandAttribute` must use the `typeof` keyword. -The `ArgumentConverterAttribute` has two constructors, one that takes the `Type` of a converter, -and one that takes the name of a converter type as a string. The string constructor is not supported -when using source generation. +The `ArgumentConverterAttribute` and `ParentCommandAttribute` have two constructors; one that takes +the `Type` of a converter or parent command, and one that takes the name of a type as a string. The +string constructor is not supported when using source generation. For example, the following code triggers this error: @@ -333,7 +333,7 @@ partial class Arguments ``` To fix this error, either use the constructor that takes a `Type` using the `typeof` keyword, or -use a `CommandLineParser` without using source generation. +do not use source generation. ### OCL0031 @@ -707,7 +707,7 @@ For example, the following code triggers this warning: [GeneratedParser] partial class Arguments { - /// WARNING: No DescriptionAttribute on this member. + // WARNING: No DescriptionAttribute on this member. [CommandLineAttribute] public string? Argument { get; set; } } @@ -737,7 +737,7 @@ the usage help. For example, the following code triggers this warning: ```csharp -/// WARNING: No DescriptionAttribute on this subcommand class. +// WARNING: No DescriptionAttribute on this subcommand class. [GeneratedParser] [Command] partial class MyCommand : ICommand @@ -765,3 +765,50 @@ partial class MyCommand : ICommand This warning will not be emitted for subcommands that are hidden using the `CommandAttribute.IsHidden` property. + +### OCL0035 + +The `ParentCommandAttribute` attribute is only used for subcommands, but was used on an arguments +type that isn't a subcommand. + +For example, the following code triggers this warning: + +```csharp +// WARNING: ParentCommandAttribute is ignored for non-commands. +[GeneratedParser] +[ParentCommand(typeof(SomeCommand))] +partial class MyCommand +{ + [CommandLineAttribute] + [Description("A description of the argument.")] + public string? Argument { get; set; } +} +``` + +### OCL0036 + +The `ApplicationFriendlyNameAttribute` attribute was used on a subcommand. The +`ApplicationFriendlyNameAttribute` is used by the automatic `-Version` argument, which is not +created for subcommands, and the automatic `version` command only uses the +`ApplicationFriendlyNameAttribute` when applied to the entry assembly for the application. + +For example, the following code triggers this warning: + +```csharp +// WARNING: ApplicationFriendlyName is ignored for commands. +[GeneratedParser] +[ApplicationFriendlyName("My Application")] +[Command] +partial class MyCommand : ICommand +{ + [CommandLineAttribute] + [Description("A description of the argument.")] + public string? Argument { get; set; } +} +``` + +Instead, the attribute should be applied to the assembly: + +```csharp +[assembly: ApplicationFriendlyName("My Application")] +``` diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index fe443a9e..7883ea9b 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -140,6 +140,14 @@ public static Diagnostic ArgumentConverterStringNotSupported(AttributeData attri attribute.GetLocation(), symbol.ToDisplayString()); + public static Diagnostic ParentCommandStringNotSupported(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( + "OCL0015", // Intentially the same as above. + nameof(Resources.ParentCommandStringNotSupportedTitle), + nameof(Resources.ParentCommandStringNotSupportedMessageFormat), + DiagnosticSeverity.Error, + attribute.GetLocation(), + symbol.ToDisplayString()); + public static Diagnostic IgnoredAttribute(ISymbol symbol, AttributeData attribute) => CreateDiagnostic( "OCL0016", nameof(Resources.UnknownAttributeTitle), @@ -316,11 +324,20 @@ public static Diagnostic CommandWithoutDescription(ISymbol symbol) => CreateDiag symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); - public static Diagnostic ParentCommandStringNotSupported(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( - "OCL0015", - nameof(Resources.ParentCommandStringNotSupportedTitle), - nameof(Resources.ParentCommandStringNotSupportedMessageFormat), - DiagnosticSeverity.Error, + public static Diagnostic IgnoredAttributeForNonCommand(ISymbol symbol, AttributeData attribute) => CreateDiagnostic( + "OCL0035", + nameof(Resources.IgnoredAttributeForNonCommandTitle), + nameof(Resources.IgnoredAttributeForNonCommandMessageFormat), + DiagnosticSeverity.Warning, + attribute.GetLocation(), + attribute.AttributeClass?.ToDisplayString(), + symbol.ToDisplayString()); + + public static Diagnostic IgnoredFriendlyNameAttribute(ISymbol symbol, AttributeData attribute) => CreateDiagnostic( + "OCL0036", + nameof(Resources.IgnoredFriendlyNameAttributeTitle), + nameof(Resources.IgnoredFriendlyNameAttributeMessageFormat), + DiagnosticSeverity.Warning, attribute.GetLocation(), symbol.ToDisplayString()); diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index f4cb5802..06243988 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -124,12 +124,27 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen return null; } - if (isCommand && attributes.Description == null) + if (isCommand) { - var commandInfo = new CommandAttributeInfo(attributes.Command!); - if (!commandInfo.IsHidden) + if (attributes.Description == null) { - _context.ReportDiagnostic(Diagnostics.CommandWithoutDescription(_argumentsClass)); + 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)); } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 3def6fbc..01c56011 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -312,6 +312,24 @@ internal static string IgnoredAttributeForDictionaryWithConverterTitle { } } + /// + /// 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 commands.. + /// + 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}.. /// @@ -348,6 +366,24 @@ internal static string IgnoredAttributeForNonMultiValueTitle { } } + /// + /// 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 multi-value command line argument defined by {0}.{1} must have an array rank of one.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 361962db..63fe4f63 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -201,6 +201,12 @@ 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 commands. + The {0} attribute is ignored for the non-dictionary argument defined by {1}. @@ -213,6 +219,12 @@ The attribute is not used for a non-dictionary argument. + + The ApplicationFriendlyNameAttribute on '{0}' is ignored because '{0}' is a subcommand. Use '[assembly: ApplicationFriendlyName(...)]' instead. + + + The ApplicationFriendlyNameAttribute is ignored on a subcommand. + The multi-value command line argument defined by {0}.{1} must have an array rank of one. From eb1a6ec7f0c46d6440b6af5932425d40d56ada00 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 8 Jun 2023 18:21:29 -0700 Subject: [PATCH 113/234] Auto prefix aliases for commands. --- docs/Migrating.md | 3 ++ src/Ookii.CommandLine.Tests/SubCommandTest.cs | 23 +++++++++- src/Ookii.CommandLine/Commands/CommandInfo.cs | 37 ++++++++++++---- .../Commands/CommandManager.cs | 43 +++++++++++++------ .../Commands/CommandOptions.cs | 28 ++++++++++-- src/Ookii.CommandLine/ParseOptions.cs | 2 +- .../Properties/Resources.Designer.cs | 9 ++++ .../Properties/Resources.resx | 3 ++ .../StringComparisonExtensions.cs | 24 +++++++++++ src/Ookii.CommandLine/UsageWriter.cs | 10 +---- src/Samples/TopLevelArguments/Program.cs | 4 +- 11 files changed, 147 insertions(+), 39 deletions(-) create mode 100644 src/Ookii.CommandLine/StringComparisonExtensions.cs diff --git a/docs/Migrating.md b/docs/Migrating.md index 74da633f..1da2b77d 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -32,6 +32,9 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ access modifiers, and to make sure generated and reflection-based command managers behave the same. - The `CommandInfo` type is now a class instead of a structure. +- `ParseOptions.ArgumentNameComparer` and `CommandOptions.CommandNameComparer` have been replaced + by `ArgumentNameComparison` and `CommandNameComparison` respectively, both now taking a + `StringComparison` value instead of a `IComparer`. ## Breaking API changes from version 2.4 diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index f316d7ec..31e1af90 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -61,7 +61,7 @@ public void GetCommandTest(ProviderKind kind) Assert.AreEqual("test", command.Name); Assert.AreEqual(typeof(TestCommand), command.CommandType); - var manager2 = new CommandManager(_commandAssembly, new CommandOptions() { CommandNameComparer = StringComparer.Ordinal }); + var manager2 = new CommandManager(_commandAssembly, new CommandOptions() { CommandNameComparison = StringComparison.Ordinal, AutoCommandPrefixAliases = false }); command = manager2.GetCommand("Test"); Assert.IsNull(command); @@ -384,7 +384,8 @@ public void TestParentCommand(ProviderKind kind) { var options = new CommandOptions { - ParentCommand = typeof(TestParentCommand) + ParentCommand = typeof(TestParentCommand), + AutoCommandPrefixAliases = false, }; var manager = CreateManager(kind, options); @@ -441,6 +442,24 @@ public void TestParentCommandUsage(ProviderKind kind) Assert.AreEqual(_expectedNestedChildCommandUsage, writer.ToString()); } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestAutoPrefixAliases(ProviderKind kind) + { + var manager = CreateManager(kind); + + // Ambiguous between test and TestParentCommand. + Assert.IsNull(manager.GetCommand("tes")); + + // Not ambiguous + Assert.AreEqual("TestParentCommand", manager.GetCommand("testp").Name); + Assert.AreEqual("version", manager.GetCommand("v").Name); + + // Case sensitive, "tes" is no longer ambigous. + manager = CreateManager(kind, new CommandOptions() { CommandNameComparison = StringComparison.Ordinal }); + Assert.AreEqual("test", manager.GetCommand("tes").Name); + } + private record struct ExpectedCommand(string Name, Type Type, bool CustomParsing = false, params string[] Aliases) { diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index cf28096b..092bfdc6 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -269,10 +269,6 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// 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. @@ -280,20 +276,45 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// /// is . /// - public bool MatchesName(string name, IComparer? comparer = null) + public bool MatchesName(string name) { if (name == null) { throw new ArgumentNullException(nameof(name)); } - comparer ??= StringComparer.OrdinalIgnoreCase; - if (comparer.Compare(name, _name) == 0) + if (string.Equals(name, _name, Manager.Options.CommandNameComparison)) + { + return true; + } + + 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 the is a prefix of the + /// property or any of the items in the property. + /// + /// + /// is . + /// + public bool MatchesPrefix(string prefix) + { + if (prefix == null) + { + throw new ArgumentNullException(nameof(prefix)); + } + + if (Name.StartsWith(prefix, Manager.Options.CommandNameComparison)) { return true; } - return Aliases.Any(alias => comparer.Compare(name, alias) == 0); + return Aliases.Any(alias => alias.StartsWith(prefix, Manager.Options.CommandNameComparison)); } /// diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index 94637314..c9d4aaf9 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -236,7 +236,7 @@ public IEnumerable GetCommands() var commands = GetCommandsUnsortedAndFiltered(); if (_options.AutoVersionCommand && _options.ParentCommand == null && - !commands.Any(c => _options.CommandNameComparer.Compare(c.Name, Properties.Resources.AutomaticVersionCommandName) == 0)) + !commands.Any(c => c.MatchesName(Properties.Resources.AutomaticVersionCommandName))) { var versionCommand = CommandInfo.GetAutomaticVersionCommand(this); if (Options.CommandFilter?.Invoke(versionCommand) ?? true) @@ -245,7 +245,7 @@ public IEnumerable GetCommands() } } - return commands.OrderBy(c => c.Name, _options.CommandNameComparer); + return commands.OrderBy(c => c.Name, _options.CommandNameComparison.GetComparer()); } /// @@ -295,27 +295,42 @@ public IEnumerable GetCommands() throw new ArgumentNullException(nameof(commandName)); } - var command = GetCommandsUnsortedAndFiltered() - .Where(c => c.MatchesName(commandName, Options.CommandNameComparer)) - .FirstOrDefault(); - - if (command != null) + var commands = GetCommandsUnsortedAndFiltered(); + if (_options.AutoVersionCommand && _options.ParentCommand == null) { - return command; + // We can unconditionally append this since it will not be checked if an earlier + // command matches. + // TODO: I don't think this logic is correct if there is a command with the same name + // and we match it by prefix only. + commands = commands.Append(CommandInfo.GetAutomaticVersionCommand(this)); } - if (_options.AutoVersionCommand && - _options.ParentCommand == null && - _options.CommandNameComparer.Compare(commandName, _options.AutoVersionCommandName()) == 0) + CommandInfo? partialMatch = null; + var ambiguousMatch = false; + foreach (var command in commands) { - command = CommandInfo.GetAutomaticVersionCommand(this); - if (_options.CommandFilter?.Invoke(command) ?? true) + // Check for an exact match. + if (command.MatchesName(commandName)) { return command; } + + if (Options.AutoCommandPrefixAliases && !ambiguousMatch && command.MatchesPrefix(commandName)) + { + if (partialMatch == null) + { + partialMatch = command; + } + else + { + // The prefix is ambigious, so don't use it. + partialMatch = null; + ambiguousMatch = true; + } + } } - return null; + return partialMatch; } /// diff --git a/src/Ookii.CommandLine/Commands/CommandOptions.cs b/src/Ookii.CommandLine/Commands/CommandOptions.cs index 0868d944..81feefd8 100644 --- a/src/Ookii.CommandLine/Commands/CommandOptions.cs +++ b/src/Ookii.CommandLine/Commands/CommandOptions.cs @@ -10,12 +10,13 @@ namespace Ookii.CommandLine.Commands public class CommandOptions : ParseOptions { /// - /// Gets or sets the used to compare command names. + /// Gets or set the type of string comparison to use for argument names. /// /// - /// The used to compare command names. The default value is . + /// One of the values of the enumeration. The default value + /// is . /// - public IComparer CommandNameComparer { get; set; } = StringComparer.OrdinalIgnoreCase; + public StringComparison CommandNameComparison { get; set; } = StringComparison.OrdinalIgnoreCase; /// /// Gets or sets a value that indicates how names are created for commands that don't have @@ -130,6 +131,27 @@ public class CommandOptions : ParseOptions /// public bool AutoVersionCommand { get; set; } = true; + /// + /// Gets or sets a value that indicates whether unique prefixes of a command name 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 argument. 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()); diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 838d55e5..cb63e675 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -337,7 +337,7 @@ public class ParseOptions /// 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 "Port" would be an alias for "Port, and "Pr" an alias for + /// 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. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 3fbffa80..08eedbae 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -384,6 +384,15 @@ 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 'minimum' and 'maximum' parameters cannot both be null.. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index 1f884881..80a87528 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -402,4 +402,7 @@ The arguments type {0} has the GeneratedParserAttribute applied, but reflection is being used to create a parser for it. Use the generated {0}.CreateParser() or {0}.Parse() methods to use the generated parser. For subcommands, use the GeneratedCommandManagerAttribute. + + Invalid value for the StringComparison enumeration. + \ No newline at end of file diff --git a/src/Ookii.CommandLine/StringComparisonExtensions.cs b/src/Ookii.CommandLine/StringComparisonExtensions.cs new file mode 100644 index 00000000..e508b1a6 --- /dev/null +++ b/src/Ookii.CommandLine/StringComparisonExtensions.cs @@ -0,0 +1,24 @@ +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 + } +} diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index 1a4bdf54..f92932ea 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -1644,15 +1644,7 @@ protected virtual IEnumerable GetArgumentsInDescriptionOrde _ => false, }); - var comparer = Parser.ArgumentNameComparison switch - { - StringComparison.CurrentCulture => StringComparer.CurrentCulture, - StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase, - StringComparison.InvariantCulture => StringComparer.InvariantCulture, - StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase, - StringComparison.Ordinal => StringComparer.Ordinal, - _ => StringComparer.OrdinalIgnoreCase, - }; + var comparer = Parser.ArgumentNameComparison.GetComparer(); return ArgumentDescriptionListOrder switch { diff --git a/src/Samples/TopLevelArguments/Program.cs b/src/Samples/TopLevelArguments/Program.cs index 398375f3..11151f32 100644 --- a/src/Samples/TopLevelArguments/Program.cs +++ b/src/Samples/TopLevelArguments/Program.cs @@ -19,10 +19,10 @@ static async Task Main() { // Use POSIX rules Mode = ParsingMode.LongShort, - ArgumentNameComparison = StringComparison.InvariantCultureIgnoreCase, + ArgumentNameComparison = StringComparison.InvariantCulture, ArgumentNameTransform = NameTransform.DashCase, ValueDescriptionTransform = NameTransform.DashCase, - CommandNameComparer = StringComparer.InvariantCultureIgnoreCase, + CommandNameComparison = StringComparison.InvariantCulture, CommandNameTransform = NameTransform.DashCase, // The top-level arguments will have a -Version argument, so no need for a version // command. From 1c6cb992b497992c5a099ed239f959989ce3485a Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 9 Jun 2023 10:49:17 -0700 Subject: [PATCH 114/234] Automatic version command bug fixes. --- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 55 +++++++++++++++++++ .../Commands/CommandManager.cs | 29 +++++++--- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 31e1af90..d97c2a52 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -460,6 +460,61 @@ public void TestAutoPrefixAliases(ProviderKind kind) Assert.AreEqual("test", manager.GetCommand("tes").Name); } + private class VersionCommandStringProvider : LocalizedStringProvider + { + public override string AutomaticVersionCommandName() => "AnotherSimpleCommand"; + } + + [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(), + }; + + var manager = CreateManager(kind, options); + + // There is no command named version. + Assert.IsNull(manager.GetCommand("version")); + + // Name returns our command. + Assert.AreEqual(typeof(AnotherSimpleCommand), manager.GetCommand("AnotherSimpleCommand").CommandType); + + // There is only one in the list of commands. + Assert.AreEqual(1, manager.GetCommands().Where(c => c.Name == "AnotherSimpleCommand").Count()); + + // Prefix is not ambiguous because the automatic command doesn't exist. + Assert.AreEqual(typeof(AnotherSimpleCommand), manager.GetCommand("Another").CommandType); + + // 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").SingleOrDefault().CommandType); + } + + [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")); + } private record struct ExpectedCommand(string Name, Type Type, bool CustomParsing = false, params string[] Aliases) { diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index c9d4aaf9..fa003bed 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -236,7 +236,7 @@ public IEnumerable GetCommands() var commands = GetCommandsUnsortedAndFiltered(); if (_options.AutoVersionCommand && _options.ParentCommand == null && - !commands.Any(c => c.MatchesName(Properties.Resources.AutomaticVersionCommandName))) + !commands.Any(c => c.MatchesName(_options.StringProvider.AutomaticVersionCommandName()))) { var versionCommand = CommandInfo.GetAutomaticVersionCommand(this); if (Options.CommandFilter?.Invoke(versionCommand) ?? true) @@ -266,10 +266,18 @@ public IEnumerable GetCommands() /// 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. + /// property. A command's aliases are specified using the + /// attribute. /// /// /// Commands that don't meet the criteria of the @@ -296,13 +304,16 @@ public IEnumerable GetCommands() } var commands = GetCommandsUnsortedAndFiltered(); + CommandInfo? versionCommand = null; if (_options.AutoVersionCommand && _options.ParentCommand == null) { - // We can unconditionally append this since it will not be checked if an earlier - // command matches. - // TODO: I don't think this logic is correct if there is a command with the same name - // and we match it by prefix only. - commands = commands.Append(CommandInfo.GetAutomaticVersionCommand(this)); + // 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) + { + commands = commands.Append(versionCommand); + } } CommandInfo? partialMatch = null; @@ -321,9 +332,11 @@ public IEnumerable GetCommands() { partialMatch = command; } - else + 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; } From 1bff8754b3049b7b022bd40e9b334b1e5ce63a4c Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 9 Jun 2023 10:52:14 -0700 Subject: [PATCH 115/234] Test automatic version command with parent command. --- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 6 ++++++ src/Ookii.CommandLine/Commands/CommandManager.cs | 1 + 2 files changed, 7 insertions(+) diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index d97c2a52..b498155b 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -514,6 +514,12 @@ public void TestNoVersionCommand(ProviderKind kind) 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")); } private record struct ExpectedCommand(string Name, Type Type, bool CustomParsing = false, params string[] Aliases) diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index fa003bed..1728d0a4 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -326,6 +326,7 @@ public IEnumerable GetCommands() return command; } + // Check for a prefix match, if requested. if (Options.AutoCommandPrefixAliases && !ambiguousMatch && command.MatchesPrefix(commandName)) { if (partialMatch == null) From a16d00e4b99b22380a446f085487d9207580ec21 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 9 Jun 2023 11:23:41 -0700 Subject: [PATCH 116/234] Helper for using TypeConverter. --- docs/Migrating.md | 8 +-- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 1 + .../CommandLineParserTest.cs | 2 + src/Ookii.CommandLine/CommandLineParser.cs | 2 +- .../CommandLineParserGeneric.cs | 2 +- .../TypeConverterArgumentConverter.cs | 51 +++++++++++++++++++ .../TypeConverterArgumentConverterGeneric.cs | 23 +++++++++ .../Properties/Resources.Designer.cs | 9 ++++ .../Properties/Resources.resx | 3 ++ 9 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs create mode 100644 src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverterGeneric.cs diff --git a/docs/Migrating.md b/docs/Migrating.md index 1da2b77d..fa1f1598 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -23,6 +23,8 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ - This change enables more flexibility, better performance by supporting conversions using `ReadOnlySpan`, and enables trimming your assembly when combined with [source generation](SourceGeneration.md). + - If you have existing conversions that depend on a `TypeConverter`, use the + `TypeConverterArgumentConverter` as a convenient way to keep using that conversion. - Constructor parameters can no longer be used to define command line arguments. Instead, all arguments must be defined using properties. If you were using constructor parameters to avoid setting a default value for a non-nullable reference type, you can use the `required` keyword @@ -31,10 +33,10 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ 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. -- The `CommandInfo` type is now a class instead of a structure. - `ParseOptions.ArgumentNameComparer` and `CommandOptions.CommandNameComparer` have been replaced by `ArgumentNameComparison` and `CommandNameComparison` respectively, both now taking a - `StringComparison` value instead of a `IComparer`. + `StringComparison` value instead of an `IComparer`. +- The `CommandInfo` type is now a class instead of a structure. ## Breaking API changes from version 2.4 @@ -79,7 +81,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 diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index aaf4d349..ba9d2010 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -45,6 +45,7 @@ partial class TestArguments [CommandLineArgument("other2", DefaultValue = "47", Position = 5), Description("Arg4 description.")] [ValueDescription("Number")] [ValidateRange(0, 1000, IncludeInUsageHelp = false)] + [ArgumentConverter(typeof(TypeConverterArgumentConverter))] public int Arg4 { get; set; } // Short/long name stuff should be ignored if not using LongShort mode. diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 0a5c9713..8d5595e0 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1295,6 +1295,8 @@ private static void VerifyArguments(IEnumerable arguments, VerifyArgument(arg, expected[index]); ++index; } + + 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 = 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) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 0b7ace02..ceaf4588 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -315,7 +315,7 @@ private struct PrefixInfo /// /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] + [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] #endif public CommandLineParser(Type argumentsType, ParseOptions? options = null) : this(new ReflectionArgumentProvider(argumentsType ?? throw new ArgumentNullException(nameof(argumentsType))), options) diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index a7a88507..ff81bf3e 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -51,7 +51,7 @@ public class CommandLineParser : CommandLineParser /// /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] + [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] #endif public CommandLineParser(ParseOptions? options = null) : base(typeof(T), options) diff --git a/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs b/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs new file mode 100644 index 00000000..9c4113ec --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Ookii.CommandLine.Conversion; + +/// +/// A that wraps an existing for a +/// type. +/// +/// +/// +/// For a convenient way to use the default for a type, use the +/// class. +/// +/// +public class TypeConverterArgumentConverter : ArgumentConverter +{ + /// + /// Initializes a new instance of the class. + /// + /// The to use. + /// + /// is . + /// + /// + /// The specified by cannot convert + /// from a . + /// + public TypeConverterArgumentConverter(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/TypeConverterArgumentConverterGeneric.cs b/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverterGeneric.cs new file mode 100644 index 00000000..1269e43e --- /dev/null +++ b/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverterGeneric.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Ookii.CommandLine.Conversion; + +/// +/// An that wraps the default for a +/// type. +/// +/// The type to convert to. +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode("Determining the TypeConverter for a type may require the type to be annotated.")] +#endif +public class TypeConverterArgumentConverter : TypeConverterArgumentConverter +{ + /// + /// Initializes a new instance of the class. + /// + public TypeConverterArgumentConverter() + : base(TypeDescriptor.GetConverter(typeof(T))) + { + } +} diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 08eedbae..1fbe3ea6 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -393,6 +393,15 @@ internal static string InvalidStringComparison { } } + /// + /// 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.. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index 80a87528..9c0349ff 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -405,4 +405,7 @@ Invalid value for the StringComparison enumeration. + + The specified TypeConverter cannot converter from a string. + \ No newline at end of file From 2d426f0f1e39caacbf9bc11aa034b1e8079ccb0a Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 9 Jun 2023 11:41:36 -0700 Subject: [PATCH 117/234] Generate Parse(ReadOnlyMemory) overload. --- src/Ookii.CommandLine.Generator/ParserGenerator.cs | 2 +- src/Ookii.CommandLine/CommandLineParserGeneric.cs | 12 ++++++++++++ src/Ookii.CommandLine/IParser.cs | 3 +-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 06243988..db183aa5 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -160,7 +160,7 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen _builder.AppendLine(); _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(string[] args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); _builder.AppendLine(); - _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(string[] args, int index, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args, index);"); + _builder.AppendLine($"public static {nullableType.ToQualifiedName()} Parse(System.ReadOnlyMemory args, Ookii.CommandLine.ParseOptions? options = null) => CreateParser(options).ParseWithErrorHandling(args);"); _builder.CloseBlock(); // class } diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index ff81bf3e..f943b754 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -111,6 +111,12 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null return (T?)base.Parse(args, index); } + /// + public new T? Parse(ReadOnlyMemory args) + { + return (T?)base.Parse(args); + } + /// public new T? ParseWithErrorHandling() { @@ -122,5 +128,11 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null { return (T?)base.ParseWithErrorHandling(args, index); } + + /// + public new T? ParseWithErrorHandling(ReadOnlyMemory args) + { + return (T?)base.ParseWithErrorHandling(args); + } } } diff --git a/src/Ookii.CommandLine/IParser.cs b/src/Ookii.CommandLine/IParser.cs index 9d683ea9..7c1dd180 100644 --- a/src/Ookii.CommandLine/IParser.cs +++ b/src/Ookii.CommandLine/IParser.cs @@ -60,7 +60,6 @@ public interface IParser : IParserProvider /// type . /// /// 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. @@ -71,7 +70,7 @@ public interface IParser : IParserProvider /// property or a method argument that returned . /// /// - public static abstract TSelf? Parse(string[] args, int index, ParseOptions? options = null); + public static abstract TSelf? Parse(ReadOnlyMemory args, ParseOptions? options = null); } #endif From dafccc14fd58c584415a7fba55be485185a3e02e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Sat, 10 Jun 2023 17:24:33 -0700 Subject: [PATCH 118/234] Direct CommandLineParser usage also uses generated provider if possible. --- .../ParserGenerator.cs | 8 +- .../ArgumentValidatorTest.cs | 8 - .../CommandLineParserNullableTest.cs | 351 ++- .../CommandLineParserTest.cs | 2580 +++++++++-------- .../KeyValuePairConverterTest.cs | 57 +- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 6 +- src/Ookii.CommandLine/CommandLineParser.cs | 95 +- .../CommandLineParserGeneric.cs | 21 +- .../GeneratedParserAttribute.cs | 8 +- src/Ookii.CommandLine/ParseOptions.cs | 28 +- .../Support/ReflectionCommandInfo.cs | 10 - 11 files changed, 1556 insertions(+), 1616 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index db183aa5..2108499c 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -148,7 +148,7 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen } } - _builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{_argumentsClass.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new GeneratedProvider(), options);"); + _builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{_argumentsClass.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new OokiiCommandLineArgumentProvider(), options);"); _builder.AppendLine(); var nullableType = _argumentsClass.WithNullableAnnotation(NullableAnnotation.Annotated); @@ -169,9 +169,9 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isCommand) { - _builder.AppendLine("private class GeneratedProvider : Ookii.CommandLine.Support.GeneratedArgumentProvider"); + _builder.AppendLine("private class OokiiCommandLineArgumentProvider : Ookii.CommandLine.Support.GeneratedArgumentProvider"); _builder.OpenBlock(); - _builder.AppendLine("public GeneratedProvider()"); + _builder.AppendLine("public OokiiCommandLineArgumentProvider()"); _builder.IncreaseIndent(); _builder.AppendLine(": base("); _builder.IncreaseIndent(); @@ -250,7 +250,7 @@ private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isComman } _builder.CloseBlock(); // CreateInstance() - _builder.CloseBlock(); // GeneratedProvider class + _builder.CloseBlock(); // OokiiCommandLineArgumentProvider class return !hasError; } diff --git a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs index 551811e3..582583b6 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs @@ -14,14 +14,6 @@ public class ArgumentValidatorTest CommandLineParser _parser; CommandLineArgument _argument; - [ClassInitialize] - public static void TestFixtureSetup(TestContext context) - { - // Avoid exception when testing reflection on argument types that also have the - // GeneratedParseAttribute set. - ParseOptions.AllowReflectionWithGeneratedParserDefault = true; - } - [TestInitialize] public void Initialize() { diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs index 0e0769fd..ce643bf6 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -11,202 +11,201 @@ 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) { - [ClassInitialize] - public static void TestFixtureSetup(TestContext context) - { - // Avoid exception when testing reflection on argument types that also have the - // GeneratedParseAttribute set. - ParseOptions.AllowReflectionWithGeneratedParserDefault = true; - } + // Get test coverage of reflection provider even on types that have the + // GeneratedParserAttribute. + ParseOptions.ForceReflectionDefault = true; + } - [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); - } + [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); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNonNullableConstructor(ProviderKind kind) - { - // TODO: Update for new ctor arguments style. - var parser = CommandLineParserTest.CreateParser(kind); - 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); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNonNullableConstructor(ProviderKind kind) + { + // TODO: Update for new ctor arguments style. + var parser = CommandLineParserTest.CreateParser(kind); + 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); + } - [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] + [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] - [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); - } + [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); + } - [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); - } + [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); + } #if NET7_0_OR_GREATER - [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] + [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); + } #endif - private static void ExpectNullException(CommandLineParser parser, string argumentName, params string[] args) + private static void ExpectNullException(CommandLineParser parser, string argumentName, params string[] args) + { + try { - try - { - parser.Parse(args); - Assert.Fail("Expected exception not thrown."); - } - catch (CommandLineArgumentException ex) - { - Assert.AreEqual(CommandLineArgumentErrorCategory.NullArgumentValue, ex.Category); - Assert.AreEqual(argumentName, ex.ArgumentName); - } + parser.Parse(args); + Assert.Fail("Expected exception not thrown."); } - - private static T ExpectSuccess(CommandLineParser parser, params string[] args) - where T : class + catch (CommandLineArgumentException ex) { - var result = parser.Parse(args); - Assert.IsNotNull(result); - return result; + Assert.AreEqual(CommandLineArgumentErrorCategory.NullArgumentValue, ex.Category); + Assert.AreEqual(argumentName, ex.ArgumentName); } + } - public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data) - => $"{methodInfo.Name} ({data[0]})"; + 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]})"; - public static IEnumerable ProviderKinds - => new[] - { - new object[] { ProviderKind.Reflection }, - new object[] { ProviderKind.Generated } - }; - } + + public static IEnumerable ProviderKinds + => new[] + { + new object[] { ProviderKind.Reflection }, + new object[] { ProviderKind.Generated } + }; } #endif diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 8d5595e0..90bc8044 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -10,1466 +10,1480 @@ 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) { - [ClassInitialize] - public static void TestFixtureSetup(TestContext context) - { - // Avoid exception when testing reflection on argument types that also have the - // GeneratedParseAttribute set. - ParseOptions.AllowReflectionWithGeneratedParserDefault = true; - } + // Get test coverage of reflection provider even on types that have the + // GeneratedParserAttribute. + ParseOptions.ForceReflectionDefault = true; + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ConstructorEmptyArgumentsTest(ProviderKind kind) + [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(Assembly.GetExecutingAssembly().GetName().Name, target.ApplicationFriendlyName); + Assert.AreEqual(string.Empty, target.Description); + Assert.AreEqual(2, target.Arguments.Count); + using var args = target.Arguments.GetEnumerator(); + VerifyArguments(target.Arguments, new[] { - 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(Assembly.GetExecutingAssembly().GetName().Name, target.ApplicationFriendlyName); - Assert.AreEqual(string.Empty, target.Description); - Assert.AreEqual(2, target.Arguments.Count); - using var args = target.Arguments.GetEnumerator(); - VerifyArguments(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] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ConstructorTest(ProviderKind kind) + [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.Count); + VerifyArguments(target.Arguments, new[] { - 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.Count); - VerifyArguments(target.Arguments, new[] - { - 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." }, - 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." }, + 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] - [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] + public void TestConstructorGeneratedProvider() + { + // Modify the default instead of explicitly creating options to make sure that the default + // is correct. + ParseOptions.ForceReflectionDefault = false; - [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); - } + // The constructor should find and use the generated provider. + var parser = new CommandLineParser(); + Assert.AreEqual(ProviderKind.Generated, parser.ProviderKind); - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestTooManyArguments(ProviderKind kind) - { - var target = CreateParser(kind); + // Change back for other tests. + ParseOptions.ForceReflectionDefault = true; + } - // Only accepts one positional argument. - CheckThrows(target, new[] { "Foo", "Bar" }, CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 1); - } + [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 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 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 ParseTestConstructorThrows(ProviderKind kind) - { - var target = CreateParser(kind); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestTooManyArguments(ProviderKind kind) + { + var target = CreateParser(kind); - CheckThrows(target, - Array.Empty(), - CommandLineArgumentErrorCategory.CreateArgumentsTypeError, - null, - typeof(ArgumentException)); - } + // Only accepts one positional argument. + CheckThrows(target, new[] { "Foo", "Bar" }, CommandLineArgumentErrorCategory.TooManyArguments, remainingArgumentCount: 1); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestDuplicateDictionaryKeys(ProviderKind kind) - { - var target = CreateParser(kind); - - DictionaryArguments 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] + [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 ParseTestMultiValueSeparator(ProviderKind kind) - { - var target = CreateParser(kind); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestConstructorThrows(ProviderKind kind) + { + var target = CreateParser(kind); - MultiValueSeparatorArguments 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); - } + CheckThrows(target, + Array.Empty(), + CommandLineArgumentErrorCategory.CreateArgumentsTypeError, + null, + typeof(ArgumentException)); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestNameValueSeparator(ProviderKind kind) - { - var target = CreateParser(kind); - Assert.AreEqual(CommandLineParser.DefaultNameValueSeparator, target.NameValueSeparator); - SimpleArguments 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); - - target.Options.NameValueSeparator = '='; - 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); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestDuplicateDictionaryKeys(ProviderKind kind) + { + var target = CreateParser(kind); + + DictionaryArguments 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] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void ParseTestKeyValueSeparator(ProviderKind kind) - { - var target = CreateParser(kind); - 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, - 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] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestMultiValueSeparator(ProviderKind kind) + { + var target = CreateParser(kind); - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsage(ProviderKind kind) - { - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; + MultiValueSeparatorArguments 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); + } - var target = CreateParser(kind, options); - var writer = new UsageWriter() - { - ExecutableName = _executableName - }; + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestNameValueSeparator(ProviderKind kind) + { + var target = CreateParser(kind); + Assert.AreEqual(CommandLineParser.DefaultNameValueSeparator, target.NameValueSeparator); + SimpleArguments 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); + + target.Options.NameValueSeparator = '='; + 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); + } - string actual = target.GetUsage(writer); - Assert.AreEqual(_expectedDefaultUsage, actual); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void ParseTestKeyValueSeparator(ProviderKind kind) + { + var target = CreateParser(kind); + 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, + 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] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageLongShort(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsage(ProviderKind kind) + { + var options = new ParseOptions() { - var target = CreateParser(kind); - var options = new UsageWriter() - { - ExecutableName = _executableName - }; - - string actual = target.GetUsage(options); - Assert.AreEqual(_expectedLongShortUsage, actual); - - options.UseShortNamesForSyntax = true; - actual = target.GetUsage(options); - Assert.AreEqual(_expectedLongShortUsageShortNameSyntax, actual); + ArgumentNamePrefixes = new[] { "/", "-" } + }; - options = new UsageWriter() - { - ExecutableName = _executableName, - UseAbbreviatedSyntax = true, - }; + var target = CreateParser(kind, options); + var writer = new UsageWriter() + { + ExecutableName = _executableName + }; - actual = target.GetUsage(options); - Assert.AreEqual(_expectedLongShortUsageAbbreviated, actual); - } + string actual = target.GetUsage(writer); + Assert.AreEqual(_expectedDefaultUsage, actual); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageFilter(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageLongShort(ProviderKind kind) + { + var target = CreateParser(kind); + var options = new UsageWriter() { - var target = CreateParser(kind); - var options = new UsageWriter() - { - ExecutableName = _executableName, - ArgumentDescriptionListFilter = DescriptionListFilterMode.Description - }; + ExecutableName = _executableName + }; - string actual = target.GetUsage(options); - Assert.AreEqual(_expectedUsageDescriptionOnly, actual); + string actual = target.GetUsage(options); + Assert.AreEqual(_expectedLongShortUsage, actual); - options.ArgumentDescriptionListFilter = DescriptionListFilterMode.All; - actual = target.GetUsage(options); - Assert.AreEqual(_expectedUsageAll, actual); + options.UseShortNamesForSyntax = true; + actual = target.GetUsage(options); + Assert.AreEqual(_expectedLongShortUsageShortNameSyntax, actual); - options.ArgumentDescriptionListFilter = DescriptionListFilterMode.None; - actual = target.GetUsage(options); - Assert.AreEqual(_expectedUsageNone, actual); - } - - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageColor(ProviderKind kind) + options = new UsageWriter() { - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" } - }; - - CommandLineParser target = CreateParser(kind, options); - var writer = new UsageWriter(useColor: true) - { - ExecutableName = _executableName, - }; - - string actual = target.GetUsage(writer); - Assert.AreEqual(_expectedUsageColor, actual); + ExecutableName = _executableName, + UseAbbreviatedSyntax = true, + }; - target = CreateParser(kind); - actual = target.GetUsage(writer); - Assert.AreEqual(_expectedLongShortUsageColor, actual); - } + actual = target.GetUsage(options); + Assert.AreEqual(_expectedLongShortUsageAbbreviated, actual); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageOrder(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageFilter(ProviderKind kind) + { + var target = CreateParser(kind); + var options = new UsageWriter() { - var parser = CreateParser(kind); - var options = new UsageWriter() - { - ExecutableName = _executableName, - ArgumentDescriptionListOrder = DescriptionListSortMode.Alphabetical, - }; + ExecutableName = _executableName, + ArgumentDescriptionListFilter = DescriptionListFilterMode.Description + }; - var usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalLongName, usage); + string actual = target.GetUsage(options); + Assert.AreEqual(_expectedUsageDescriptionOnly, actual); - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalDescending; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalLongNameDescending, usage); + options.ArgumentDescriptionListFilter = DescriptionListFilterMode.All; + actual = target.GetUsage(options); + Assert.AreEqual(_expectedUsageAll, actual); - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortName; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalShortName, usage); - - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortNameDescending; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalShortNameDescending, usage); + options.ArgumentDescriptionListFilter = DescriptionListFilterMode.None; + actual = target.GetUsage(options); + Assert.AreEqual(_expectedUsageNone, actual); + } - parser = CreateParser(kind, new ParseOptions() { Mode = ParsingMode.Default }); - options.ArgumentDescriptionListOrder = DescriptionListSortMode.Alphabetical; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabetical, usage); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageColor(ProviderKind kind) + { + var options = new ParseOptions() + { + ArgumentNamePrefixes = new[] { "/", "-" } + }; - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalDescending; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalDescending, usage); + CommandLineParser target = CreateParser(kind, options); + var writer = new UsageWriter(useColor: true) + { + ExecutableName = _executableName, + }; - // ShortName versions work like regular if not in LongShortMode. - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortName; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabetical, usage); + string actual = target.GetUsage(writer); + Assert.AreEqual(_expectedUsageColor, actual); - options.ArgumentDescriptionListOrder = DescriptionListSortMode.AlphabeticalShortNameDescending; - usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageAlphabeticalDescending, usage); - } + target = CreateParser(kind); + actual = target.GetUsage(writer); + Assert.AreEqual(_expectedLongShortUsageColor, actual); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageSeparator(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageOrder(ProviderKind kind) + { + var parser = CreateParser(kind); + var options = new UsageWriter() { - var options = new ParseOptions() - { - ArgumentNamePrefixes = new[] { "/", "-" }, - UsageWriter = new UsageWriter() - { - ExecutableName = _executableName, - UseWhiteSpaceValueSeparator = false, - } - }; - var target = CreateParser(kind, options); - string actual = target.GetUsage(options.UsageWriter); - Assert.AreEqual(_expectedUsageSeparator, actual); - } + 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] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestWriteUsageCustomIndent(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageSeparator(ProviderKind kind) + { + var options = new ParseOptions() { - var options = new ParseOptions() + ArgumentNamePrefixes = new[] { "/", "-" }, + UsageWriter = new UsageWriter() { - UsageWriter = new UsageWriter() - { - ExecutableName = _executableName, - ArgumentDescriptionIndent = 4, - } - }; - var target = CreateParser(kind, options); - string actual = target.GetUsage(options.UsageWriter); - Assert.AreEqual(_expectedCustomIndentUsage, actual); - } + ExecutableName = _executableName, + UseWhiteSpaceValueSeparator = false, + } + }; + var target = CreateParser(kind, options); + string actual = target.GetUsage(options.UsageWriter); + Assert.AreEqual(_expectedUsageSeparator, actual); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestStaticParse(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageCustomIndent(ProviderKind kind) + { + var options = new ParseOptions() { - using var output = new StringWriter(); - using var lineWriter = new LineWrappingTextWriter(output, 0); - using var error = new StringWriter(); - var options = new ParseOptions() + UsageWriter = new UsageWriter() { - ArgumentNamePrefixes = new[] { "/", "-" }, - Error = error, - UsageWriter = new UsageWriter(lineWriter) - { - 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()); - } + ExecutableName = _executableName, + ArgumentDescriptionIndent = 4, + } + }; + var target = CreateParser(kind, options); + string actual = target.GetUsage(options.UsageWriter); + Assert.AreEqual(_expectedCustomIndentUsage, actual); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestCancelParsing(ProviderKind kind) + [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() { - 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) + ArgumentNamePrefixes = new[] { "/", "-" }, + Error = error, + UsageWriter = new UsageWriter(lineWriter) { - if (e.Argument.ArgumentName == "DoesNotCancel") - { - e.CancelParsing = CancelMode.Abort; - } + 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()); + } - 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) + [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") { - if (e.Argument.ArgumentName == "DoesCancel") - { - Assert.AreEqual(CancelMode.Abort, e.CancelParsing); - e.CancelParsing = CancelMode.None; - } + e.CancelParsing = CancelMode.Abort; } - - 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); } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestCancelParsingSuccess(ProviderKind kind) + 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 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); - } - - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestParseOptionsAttribute(ProviderKind kind) - { - var parser = CreateParser(kind); - 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() + if (e.Argument.ArgumentName == "DoesCancel") { - Mode = ParsingMode.Default, - ArgumentNameComparison = StringComparison.OrdinalIgnoreCase, - AllowWhiteSpaceValueSeparator = true, - DuplicateArguments = ErrorMode.Error, - NameValueSeparator = ';', - ArgumentNamePrefixes = new[] { "+" }, - AutoHelpArgument = true, - }; - - parser = CreateParser(kind, 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")); + Assert.AreEqual(CancelMode.Abort, e.CancelParsing); + e.CancelParsing = CancelMode.None; + } } - [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); - } + 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); + } - [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 = 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, 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); - } + [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); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestMethodArguments(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestParseOptionsAttribute(ProviderKind kind) + { + var parser = CreateParser(kind); + 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() { - var parser = CreateParser(kind); + Mode = ParsingMode.Default, + ArgumentNameComparison = StringComparison.OrdinalIgnoreCase, + AllowWhiteSpaceValueSeparator = true, + DuplicateArguments = ErrorMode.Error, + NameValueSeparator = ';', + ArgumentNamePrefixes = new[] { "+" }, + AutoHelpArgument = true, + }; + + parser = CreateParser(kind, 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")); + } - 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 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); + } - CheckSuccess(parser, new[] { "-NoCancel" }); - Assert.AreEqual(nameof(MethodArguments.NoCancel), MethodArguments.CalledMethodName); + [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 = 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, 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); + } - CheckCanceled(parser, new[] { "-Cancel", "Foo" }, "Cancel", false, 1); - Assert.AreEqual(nameof(MethodArguments.Cancel), MethodArguments.CalledMethodName); + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestMethodArguments(ProviderKind kind) + { + var parser = CreateParser(kind); - CheckCanceled(parser, new[] { "-CancelWithHelp" }, "CancelWithHelp", true, 0); - Assert.AreEqual(nameof(MethodArguments.CancelWithHelp), MethodArguments.CalledMethodName); + Assert.AreEqual(ArgumentKind.Method, parser.GetArgument("NoCancel").Kind); + Assert.IsNull(parser.GetArgument("NotAnArgument")); + Assert.IsNull(parser.GetArgument("NotStatic")); + Assert.IsNull(parser.GetArgument("NotPublic")); - CheckSuccess(parser, new[] { "-CancelWithValue", "1" }); - Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); - Assert.AreEqual(1, MethodArguments.Value); + CheckSuccess(parser, new[] { "-NoCancel" }); + Assert.AreEqual(nameof(MethodArguments.NoCancel), MethodArguments.CalledMethodName); - CheckCanceled(parser, new[] { "-CancelWithValue", "-1" }, "CancelWithValue", false); - Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); - Assert.AreEqual(-1, MethodArguments.Value); + CheckCanceled(parser, new[] { "-Cancel", "Foo" }, "Cancel", false, 1); + Assert.AreEqual(nameof(MethodArguments.Cancel), MethodArguments.CalledMethodName); - CheckSuccess(parser, new[] { "-CancelWithValueAndHelp", "1" }); - Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); - Assert.AreEqual(1, MethodArguments.Value); + CheckCanceled(parser, new[] { "-CancelWithHelp" }, "CancelWithHelp", true, 0); + Assert.AreEqual(nameof(MethodArguments.CancelWithHelp), MethodArguments.CalledMethodName); - CheckCanceled(parser, new[] { "-CancelWithValueAndHelp", "-1", "bar" }, "CancelWithValueAndHelp", true, 1); - Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); - Assert.AreEqual(-1, MethodArguments.Value); + CheckSuccess(parser, new[] { "-CancelWithValue", "1" }); + Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); + Assert.AreEqual(1, MethodArguments.Value); - CheckSuccess(parser, new[] { "-NoReturn" }); - Assert.AreEqual(nameof(MethodArguments.NoReturn), MethodArguments.CalledMethodName); + CheckCanceled(parser, new[] { "-CancelWithValue", "-1" }, "CancelWithValue", false); + Assert.AreEqual(nameof(MethodArguments.CancelWithValue), MethodArguments.CalledMethodName); + Assert.AreEqual(-1, MethodArguments.Value); - CheckSuccess(parser, new[] { "42" }); - Assert.AreEqual(nameof(MethodArguments.Positional), MethodArguments.CalledMethodName); - Assert.AreEqual(42, MethodArguments.Value); + CheckSuccess(parser, new[] { "-CancelWithValueAndHelp", "1" }); + Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); + Assert.AreEqual(1, MethodArguments.Value); - CheckCanceled(parser, new[] { "-CancelModeAbort", "Foo" }, "CancelModeAbort", false, 1); - Assert.AreEqual(nameof(MethodArguments.CancelModeAbort), MethodArguments.CalledMethodName); + CheckCanceled(parser, new[] { "-CancelWithValueAndHelp", "-1", "bar" }, "CancelWithValueAndHelp", true, 1); + Assert.AreEqual(nameof(MethodArguments.CancelWithValueAndHelp), MethodArguments.CalledMethodName); + Assert.AreEqual(-1, MethodArguments.Value); - CheckSuccess(parser, new[] { "-CancelModeSuccess", "Foo" }, "CancelModeSuccess", 1); - Assert.AreEqual(nameof(MethodArguments.CancelModeSuccess), MethodArguments.CalledMethodName); + CheckSuccess(parser, new[] { "-NoReturn" }); + Assert.AreEqual(nameof(MethodArguments.NoReturn), MethodArguments.CalledMethodName); - CheckSuccess(parser, new[] { "-CancelModeNone" }); - Assert.AreEqual(nameof(MethodArguments.CancelModeNone), MethodArguments.CalledMethodName); - } + CheckSuccess(parser, new[] { "42" }); + Assert.AreEqual(nameof(MethodArguments.Positional), MethodArguments.CalledMethodName); + Assert.AreEqual(42, MethodArguments.Value); - [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))); + CheckCanceled(parser, new[] { "-CancelModeAbort", "Foo" }, "CancelModeAbort", false, 1); + Assert.AreEqual(nameof(MethodArguments.CancelModeAbort), MethodArguments.CalledMethodName); - parser = CreateParser(kind); - VerifyArgument(parser.GetShortArgument('?'), new ExpectedArgument("Foo", typeof(int)) { ShortName = '?' }); - } + CheckSuccess(parser, new[] { "-CancelModeSuccess", "Foo" }, "CancelModeSuccess", 1); + Assert.AreEqual(nameof(MethodArguments.CancelModeSuccess), MethodArguments.CalledMethodName); - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestHiddenArgument(ProviderKind kind) - { - var parser = CreateParser(kind); + CheckSuccess(parser, new[] { "-CancelModeNone" }); + Assert.AreEqual(nameof(MethodArguments.CancelModeNone), MethodArguments.CalledMethodName); + } - // Verify the hidden argument exists. - VerifyArgument(parser.GetArgument("Hidden"), new ExpectedArgument("Hidden", typeof(int)) { IsHidden = true }); + [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))); - // Verify it's not in the usage. - var options = new UsageWriter() - { - ExecutableName = _executableName, - ArgumentDescriptionListFilter = DescriptionListFilterMode.All, - }; + parser = CreateParser(kind); + VerifyArgument(parser.GetShortArgument('?'), new ExpectedArgument("Foo", typeof(int)) { ShortName = '?' }); + } - var usage = parser.GetUsage(options); - Assert.AreEqual(_expectedUsageHidden, usage); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestHiddenArgument(ProviderKind kind) + { + var parser = CreateParser(kind); - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNameTransformPascalCase(ProviderKind kind) + // Verify the hidden argument exists. + VerifyArgument(parser.GetArgument("Hidden"), new ExpectedArgument("Hidden", typeof(int)) { IsHidden = true }); + + // Verify it's not in the usage. + var options = new UsageWriter() { - var options = new ParseOptions - { - ArgumentNameTransform = NameTransform.PascalCase - }; + ExecutableName = _executableName, + ArgumentDescriptionListFilter = DescriptionListFilterMode.All, + }; - 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 }, - }); - } + var usage = parser.GetUsage(options); + Assert.AreEqual(_expectedUsageHidden, usage); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNameTransformCamelCase(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformPascalCase(ProviderKind kind) + { + var options = new ParseOptions { - var options = new ParseOptions - { - ArgumentNameTransform = NameTransform.CamelCase - }; + ArgumentNameTransform = NameTransform.PascalCase + }; - 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] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNameTransformSnakeCase(ProviderKind kind) + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] { - var options = new ParseOptions - { - ArgumentNameTransform = NameTransform.SnakeCase - }; + 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 }, + }); + } - 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 }, - }); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformCamelCase(ProviderKind kind) + { + var options = new ParseOptions + { + ArgumentNameTransform = NameTransform.CamelCase + }; - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNameTransformDashCase(ProviderKind kind) + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] { - var options = new ParseOptions - { - ArgumentNameTransform = NameTransform.DashCase - }; + 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 }, + }); + } - 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 }, - }); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformSnakeCase(ProviderKind kind) + { + var options = new ParseOptions + { + ArgumentNameTransform = NameTransform.SnakeCase + }; - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestValueDescriptionTransform(ProviderKind kind) + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] { - var options = new ParseOptions - { - ValueDescriptionTransform = NameTransform.DashCase - }; + 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 }, + }); + } - 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 }, - }); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestNameTransformDashCase(ProviderKind kind) + { + var options = new ParseOptions + { + ArgumentNameTransform = NameTransform.DashCase + }; - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestValidation(ProviderKind kind) + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] { - // 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 = parser.Parse(new[] { "-Arg1", "1" }); - Assert.AreEqual(1, result.Arg1); - result = parser.Parse(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 = parser.Parse(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); - parser.Parse(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 = 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 - // 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 = parser.Parse(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(FormatException), remainingArgumentCount: 2); - CheckThrows(parser, new[] { "-Day", "9" }, CommandLineArgumentErrorCategory.ValidationFailed, "Day", remainingArgumentCount: 2); - CheckThrows(parser, new[] { "-Day", "" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(FormatException), remainingArgumentCount: 2); - result = parser.Parse(new[] { "-Day", "1" }); - Assert.AreEqual(DayOfWeek.Monday, result.Day); - CheckThrows(parser, new[] { "-Day2", "foo" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day2", typeof(FormatException), remainingArgumentCount: 2); - CheckThrows(parser, new[] { "-Day2", "9" }, CommandLineArgumentErrorCategory.ValidationFailed, "Day2", remainingArgumentCount: 2); - 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, new[] { "-NotNull", "" }, CommandLineArgumentErrorCategory.ValidationFailed, "NotNull", remainingArgumentCount: 2); - } + 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 TestRequires(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestValueDescriptionTransform(ProviderKind kind) + { + var options = new ParseOptions { - var parser = CreateParser(kind); - - // None of these have remaining arguments because validation happens after parsing. - var result = parser.Parse(new[] { "-Address", "127.0.0.1" }); - Assert.AreEqual(IPAddress.Loopback, result.Address); - CheckThrows(parser, new[] { "-Port", "9000" }, 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, 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 = 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); - } + ValueDescriptionTransform = NameTransform.DashCase + }; - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestProhibits(ProviderKind kind) + var parser = CreateParser(kind, options); + VerifyArguments(parser.Arguments, new[] { - var parser = CreateParser(kind); + 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 }, + }); + } - var result = parser.Parse(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"); - } + [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 = parser.Parse(new[] { "-Arg1", "1" }); + Assert.AreEqual(1, result.Arg1); + result = parser.Parse(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 = parser.Parse(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); + parser.Parse(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 = 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 + // 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 = parser.Parse(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(FormatException), remainingArgumentCount: 2); + CheckThrows(parser, new[] { "-Day", "9" }, CommandLineArgumentErrorCategory.ValidationFailed, "Day", remainingArgumentCount: 2); + CheckThrows(parser, new[] { "-Day", "" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(FormatException), remainingArgumentCount: 2); + result = parser.Parse(new[] { "-Day", "1" }); + Assert.AreEqual(DayOfWeek.Monday, result.Day); + CheckThrows(parser, new[] { "-Day2", "foo" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day2", typeof(FormatException), remainingArgumentCount: 2); + CheckThrows(parser, new[] { "-Day2", "9" }, CommandLineArgumentErrorCategory.ValidationFailed, "Day2", remainingArgumentCount: 2); + 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, new[] { "-NotNull", "" }, CommandLineArgumentErrorCategory.ValidationFailed, "NotNull", remainingArgumentCount: 2); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestRequiresAny(ProviderKind kind) - { - var parser = CreateParser(kind); + [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 = parser.Parse(new[] { "-Address", "127.0.0.1" }); + Assert.AreEqual(IPAddress.Loopback, result.Address); + CheckThrows(parser, new[] { "-Port", "9000" }, 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, 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 = 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); + } - // No need to check if the arguments work indivially since TestRequires and TestProhibits already did that. - CheckThrows(parser, Array.Empty(), CommandLineArgumentErrorCategory.MissingRequiredArgument); - } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestProhibits(ProviderKind kind) + { + var parser = CreateParser(kind); + + var result = parser.Parse(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"); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestValidatorUsageHelp(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestRequiresAny(ProviderKind kind) + { + var parser = CreateParser(kind); + + // No need to check if the arguments work indivially since TestRequires and TestProhibits already did that. + CheckThrows(parser, Array.Empty(), CommandLineArgumentErrorCategory.MissingRequiredArgument); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestValidatorUsageHelp(ProviderKind kind) + { + CommandLineParser parser = CreateParser(kind); + var options = new UsageWriter() { - CommandLineParser parser = CreateParser(kind); - var options = new UsageWriter() - { - ExecutableName = _executableName, - }; + ExecutableName = _executableName, + }; - Assert.AreEqual(_expectedUsageValidators, parser.GetUsage(options)); + Assert.AreEqual(_expectedUsageValidators, parser.GetUsage(options)); - parser = CreateParser(kind); - Assert.AreEqual(_expectedUsageDependencies, parser.GetUsage(options)); + parser = CreateParser(kind); + Assert.AreEqual(_expectedUsageDependencies, parser.GetUsage(options)); - options.IncludeValidatorsInDescription = false; - Assert.AreEqual(_expectedUsageDependenciesDisabled, parser.GetUsage(options)); - } + options.IncludeValidatorsInDescription = false; + Assert.AreEqual(_expectedUsageDependenciesDisabled, parser.GetUsage(options)); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestDefaultValueDescriptions(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestDefaultValueDescriptions(ProviderKind kind) + { + var options = new ParseOptions() { - var options = new ParseOptions() + DefaultValueDescriptions = new Dictionary() { - DefaultValueDescriptions = new Dictionary() - { - { 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); - } + { 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); + } + + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestMultiValueWhiteSpaceSeparator(ProviderKind kind) + { + var parser = CreateParser(kind); + 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, 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] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestMultiValueWhiteSpaceSeparator(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestInjection(ProviderKind kind) + { + var parser = CreateParser(kind); + var result = parser.Parse(new[] { "-Arg", "1" }); + Assert.AreSame(parser, result.Parser); + Assert.AreEqual(1, result.Arg); + + // TODO: + //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); + } + + [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 = parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }); + Assert.AreEqual("bar", result.Argument1); + + bool handlerCalled = false; + bool keepOldValue = false; + EventHandler handler = (sender, e) => { - var parser = CreateParser(kind); - 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, 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); - } + 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, new[] { "-Argument1", "foo", "-Argument1", "bar" }, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1", remainingArgumentCount: 2); + 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); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestInjection(ProviderKind kind) + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestConversion(ProviderKind kind) + { + var parser = CreateParser(kind); + var result = parser.Parse("-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 = 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); + } + + [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.Count); + VerifyArguments(parser.Arguments, new[] { - var parser = CreateParser(kind); - var result = parser.Parse(new[] { "-Arg", "1" }); - Assert.AreSame(parser, result.Parser); - Assert.AreEqual(1, result.Arg); - - // TODO: - //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); - } + 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 }, + }); + } + + [TestMethod] + public void TestInitializerDefaultValues() + { + var parser = InitializerDefaultValueArguments.CreateParser(); + Assert.AreEqual("foo\tbar\"", parser.GetArgument("Arg1").DefaultValue); + Assert.AreEqual(5.5f, parser.GetArgument("Arg2").DefaultValue); + // Arg3's default value can't be used because it's not a literal. + Assert.IsNull(parser.GetArgument("Arg3").DefaultValue); + } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestDuplicateArguments(ProviderKind kind) + [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); + } + + private class ExpectedArgument + { + public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind.SingleValue) { - var parser = CreateParser(kind); - CheckThrows(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1", remainingArgumentCount: 2); - 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, new[] { "-Argument1", "foo", "-Argument1", "bar" }, CommandLineArgumentErrorCategory.DuplicateArgument, "Argument1", remainingArgumentCount: 2); - 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); + Name = name; + Type = type; + Kind = kind; } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestConversion(ProviderKind 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; } + } + + private static void VerifyArgument(CommandLineArgument argument, ExpectedArgument expected) + { + 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); + } + + private static void VerifyArguments(IEnumerable arguments, ExpectedArgument[] expected) + { + int index = 0; + foreach (var arg in arguments) { - var parser = CreateParser(kind); - var result = parser.Parse("-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 = 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); + Assert.IsTrue(index < expected.Length, "Too many arguments."); + VerifyArgument(arg, expected[index]); + ++index; } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestDerivedClass(ProviderKind kind) + 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 = 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) + { + 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 = CreateParser(kind); - Assert.AreEqual("Base class attribute.", parser.Description); - Assert.AreEqual(4, parser.Arguments.Count); - VerifyArguments(parser.Arguments, new[] - { - 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 }, - }); + Assert.AreEqual(0, result.Arg12.Count); } - - [TestMethod] - public void TestInitializerDefaultValues() + else { - var parser = InitializerDefaultValueArguments.CreateParser(); - Assert.AreEqual("foo\tbar\"", parser.GetArgument("Arg1").DefaultValue); - Assert.AreEqual(5.5f, parser.GetArgument("Arg2").DefaultValue); - // Arg3's default value can't be used because it's not a literal. - Assert.IsNull(parser.GetArgument("Arg3").DefaultValue); + CollectionAssert.AreEqual(arg12, result.Arg12); } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestAutoPrefixAliases(ProviderKind kind) + CollectionAssert.AreEqual(arg13, result.Arg13); + if (arg14 == null) { - 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); + Assert.AreEqual(0, result.Arg14.Count); } - - private class ExpectedArgument + else { - 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; } + CollectionAssert.AreEqual(arg14, (System.Collections.ICollection)result.Arg14); } - private static void VerifyArgument(CommandLineArgument argument, ExpectedArgument expected) + if (arg15 == null) { - 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(default, result.Arg15); } - - private static void VerifyArguments(IEnumerable arguments, ExpectedArgument[] expected) + else { - int index = 0; - foreach (var arg in arguments) - { - Assert.IsTrue(index < expected.Length, "Too many arguments."); - VerifyArgument(arg, expected[index]); - ++index; - } - - Assert.AreEqual(expected.Length, index); + Assert.AreEqual(arg15.Value, result.Arg15); } + } - 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) + private static void CheckThrows(CommandLineParser parser, string[] arguments, CommandLineArgumentErrorCategory category, string argumentName = null, Type innerExceptionType = null, int remainingArgumentCount = 0) + { + try { - 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) - { - Assert.AreEqual(0, result.Arg12.Count); - } - else - { - CollectionAssert.AreEqual(arg12, result.Arg12); - } - - CollectionAssert.AreEqual(arg13, result.Arg13); - if (arg14 == null) - { - Assert.AreEqual(0, result.Arg14.Count); - } - else - { - CollectionAssert.AreEqual(arg14, (System.Collections.ICollection)result.Arg14); - } - - if (arg15 == null) - { - Assert.AreEqual(default, result.Arg15); - } - else - { - Assert.AreEqual(arg15.Value, result.Arg15); - } + parser.Parse(arguments); + Assert.Fail("Expected CommandLineException was not thrown."); } - - private static void CheckThrows(CommandLineParser parser, string[] arguments, CommandLineArgumentErrorCategory category, string argumentName = null, Type innerExceptionType = null, int remainingArgumentCount = 0) + catch (CommandLineArgumentException ex) { - try + 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) { - parser.Parse(arguments); - Assert.Fail("Expected CommandLineException was not thrown."); + Assert.IsNull(ex.InnerException); } - catch (CommandLineArgumentException ex) + else { - 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); - } - - var remaining = arguments.AsMemory(arguments.Length - remainingArgumentCount); - AssertMemoryEqual(remaining, parser.ParseResult.RemainingArguments); + Assert.IsInstanceOfType(ex.InnerException, innerExceptionType); } - } - 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 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; - } + 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 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) + internal static CommandLineParser CreateParser(ProviderKind kind, ParseOptions options = null) #if NET7_0_OR_GREATER - where T : class, IParserProvider + where T : class, IParserProvider #else - where T : class + where T : class #endif + { + var parser = kind switch { - var parser = kind switch - { - ProviderKind.Reflection => new CommandLineParser(options), + ProviderKind.Reflection => new CommandLineParser(options), #if NET7_0_OR_GREATER - ProviderKind.Generated => T.CreateParser(options), + ProviderKind.Generated => T.CreateParser(options), #else - ProviderKind.Generated => (CommandLineParser)typeof(T).InvokeMember("CreateParser", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { options }), + ProviderKind.Generated => (CommandLineParser)typeof(T).InvokeMember("CreateParser", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { options }), #endif - _ => throw new InvalidOperationException() - }; + _ => throw new InvalidOperationException() + }; - Assert.AreEqual(kind, parser.ProviderKind); - return parser; - } + Assert.AreEqual(kind, parser.ProviderKind); + return parser; + } - private static T StaticParse(ProviderKind kind, string[] args, ParseOptions options = null) + private static T StaticParse(ProviderKind kind, string[] args, ParseOptions options = null) #if NET7_0_OR_GREATER - where T : class, IParser + where T : class, IParser #else - where T : class + where T : class #endif + { + return kind switch { - return kind switch - { - ProviderKind.Reflection => CommandLineParser.Parse(args, options), + ProviderKind.Reflection => CommandLineParser.Parse(args, options), #if NET7_0_OR_GREATER - ProviderKind.Generated => T.Parse(args, options), + 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 }), + ProviderKind.Generated => (T)typeof(T).InvokeMember("Parse", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { args, options }), #endif - _ => throw new InvalidOperationException() - }; - } + _ => throw new InvalidOperationException() + }; + } - public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data) - => $"{methodInfo.Name} ({data[0]})"; + 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 + public static IEnumerable ProviderKinds + => new[] { - if (!expected.SequenceEqual(actual)) - { - Assert.Fail($"Span not equal. Expected: {{ {string.Join(", ", expected.ToArray())} }}, Actual: {{ {string.Join(", ", actual.ToArray())} }}"); - } - } + new object[] { ProviderKind.Reflection }, + new object[] { ProviderKind.Generated } + }; - public static void AssertMemoryEqual(ReadOnlyMemory expected, ReadOnlyMemory actual) - where T : IEquatable + public static void AssertSpanEqual(ReadOnlySpan expected, ReadOnlySpan actual) + where T: IEquatable + { + if (!expected.SequenceEqual(actual)) { - AssertSpanEqual(expected.Span, actual.Span); + 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/KeyValuePairConverterTest.cs b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs index 2120737b..9d386b2b 100644 --- a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs +++ b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs @@ -3,42 +3,33 @@ 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 { - [ClassInitialize] - public static void TestFixtureSetup(TestContext context) - { - // Avoid exception when testing reflection on argument types that also have the - // GeneratedParseAttribute set. - ParseOptions.AllowReflectionWithGeneratedParserDefault = true; - } - - // Needed because SpanParsableConverter only exists on .Net 7. - private class IntConverter : ArgumentConverter - { - public override object Convert(string value, CultureInfo culture, CommandLineArgument argument) - => int.Parse(value, culture); - } + public override object Convert(string value, CultureInfo culture, CommandLineArgument argument) + => int.Parse(value, culture); + } - [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); - } + [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); + } - [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); - } + [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/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index b498155b..4df78574 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -21,9 +21,9 @@ public partial class SubCommandTest [ClassInitialize] public static void TestFixtureSetup(TestContext context) { - // Avoid exception when testing reflection on argument types that also have the - // GeneratedParseAttribute set. - ParseOptions.AllowReflectionWithGeneratedParserDefault = true; + // Get test coverage of reflection provider even on types that have the + // GeneratedParserAttribute. + ParseOptions.ForceReflectionDefault = true; } [TestMethod] diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index ceaf4588..807a6f46 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -286,22 +286,12 @@ private struct PrefixInfo /// because it violates one of the rules concerning argument names or positions, or has an argument type that cannot /// be parsed. /// - /// - /// The type indicated by has the - /// attribute applied. Use the generated static CreateParser() or Parse() - /// methods on the arguments type to access the generated parser. For subcommands, use a - /// command provider with the attribute to - /// create a that will use generated parsers for subcommands. Set - /// the property to - /// to disable this exception. - /// /// /// /// This constructor uses reflection to determine the arguments defined by the type indicated - /// by at runtime. To determine the arguments at compile - /// time instead, apply the to the arguments type - /// and use the generated static CreateParser() or Parse() methods on that type - /// instead. + /// by at runtime, unless the type has the + /// applied. In that case, you can also use the + /// generated static CreateParser() or Parse() methods on that type instead. /// /// /// If the parameter is not , the @@ -318,7 +308,7 @@ private struct PrefixInfo [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] #endif public CommandLineParser(Type argumentsType, ParseOptions? options = null) - : this(new ReflectionArgumentProvider(argumentsType ?? throw new ArgumentNullException(nameof(argumentsType))), options) + : this(GetArgumentProvider(argumentsType ?? throw new ArgumentNullException(nameof(argumentsType)), options), options) { } @@ -342,16 +332,6 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) /// positions, or has an argument type that cannot /// be parsed. /// - /// - /// The provider uses , but the type indicated by the - /// property has the - /// attribute applied. Use the generated static CreateParser() or Parse() - /// methods on the arguments type to access the generated parser. For subcommands, use a - /// command provider with the attribute to - /// create a that will use generated parsers for subcommands. Set - /// the property to - /// to disable this exception. - /// /// /// /// If the parameter is not , the @@ -368,14 +348,6 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null { _provider = provider ?? throw new ArgumentNullException(nameof(provider)); _parseOptions = options ?? new(); - if (provider.Kind == ProviderKind.Reflection && - !_parseOptions.AllowReflectionWithGeneratedParser && - Attribute.IsDefined(provider.ArgumentsType, typeof(GeneratedParserAttribute))) - { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, - Properties.Resources.ReflectionWithGeneratedParserFormat, provider.ArgumentsType.FullName)); - } - var optionsAttribute = _provider.OptionsAttribute; if (optionsAttribute != null) { @@ -918,17 +890,17 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = } /// - /// - /// 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 . - /// + /// + /// 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) { if (args == null) @@ -1068,15 +1040,6 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// - /// - /// The type indicated by has the - /// attribute applied. Use the generated static CreateParser() or Parse() - /// methods on the arguments type to access the generated parser. For subcommands, use a - /// command provider with the attribute to - /// create a that will use generated parsers for subcommands. Set - /// the property to - /// to disable this exception. - /// /// /// The cannot use as the command /// line arguments type, because it violates one of the rules concerning argument names or @@ -1108,11 +1071,10 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// method. /// /// - /// This method uses reflection to determine the arguments defined by the type indicated - /// by at runtime. To determine the arguments at compile - /// time instead, apply the to the arguments type - /// and use the generated static CreateParser() or Parse() methods on that type - /// instead. + /// This method uses reflection to determine the arguments defined by the type + /// at runtime, unless the type has the applied. In + /// that case, you can also use the generated static CreateParser() or Parse() + /// methods on that type instead. /// /// #if NET6_0_OR_GREATER @@ -1805,4 +1767,23 @@ private CommandLineArgument GetShortArgumentOrThrow(char shortName) return null; } + +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] +#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))) + { + var providerType = type.GetNestedType("OokiiCommandLineArgumentProvider", BindingFlags.NonPublic); + if (providerType != null && typeof(ArgumentProvider).IsAssignableFrom(providerType)) + { + return (ArgumentProvider)Activator.CreateInstance(providerType)!; + } + } + + return new ReflectionArgumentProvider(type); + } } diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index f943b754..088ff19a 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -38,15 +38,6 @@ public class CommandLineParser : CommandLineParser /// 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 type indicated by has the - /// attribute applied. Use the generated static CreateParser() or Parse() - /// methods on the arguments type to access the generated parser. For subcommands, use a - /// command provider with the attribute to - /// create a that will use generated parsers for subcommands. Set - /// the property to - /// to disable this exception. - /// /// /// /// @@ -60,7 +51,7 @@ public CommandLineParser(ParseOptions? options = null) /// /// Initializes a new instance of the class using the - /// specified options. + /// specified argument provider and options. /// /// /// @@ -77,16 +68,6 @@ public CommandLineParser(ParseOptions? options = null) /// The property for the /// if a different type than . /// - /// - /// The provider uses , but the type indicated by the - /// property has the - /// attribute applied. Use the generated static CreateParser() or Parse() - /// methods on the arguments type to access the generated parser. For subcommands, use a - /// command provider with the attribute to - /// create a that will use generated parsers for subcommands. Set - /// the property to - /// to disable this exception. - /// /// /// /// diff --git a/src/Ookii.CommandLine/GeneratedParserAttribute.cs b/src/Ookii.CommandLine/GeneratedParserAttribute.cs index ac156deb..edb4f93f 100644 --- a/src/Ookii.CommandLine/GeneratedParserAttribute.cs +++ b/src/Ookii.CommandLine/GeneratedParserAttribute.cs @@ -14,10 +14,10 @@ namespace Ookii.CommandLine; /// /// /// To use the generated parser, source generation will add several static methods to the target -/// class: CreateParser, and three overloads of the Parse method. You must use -/// these members, as using the class directly will throw -/// an exception unless is set to -/// . +/// class: CreateParser, and three overloads of the Parse method. Using these +/// members 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 diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index cb63e675..d5cbc353 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -519,36 +519,28 @@ public LocalizedStringProvider StringProvider /// /// Gets or sets a value that indicates whether the class - /// will allow the use of reflection with an arguments class that also has the - /// attribute. + /// will use reflection even if the command line arguments type has the + /// . /// /// - /// to allow the use of reflection when the arguments class has the + /// to force the use of reflection when the arguments class has the /// attribute; otherwise, . The /// default value is . /// /// /// - /// When this property is (the default), the - /// constructor, the - /// constructor, and the static methods - /// will throw an exception if they are used with an arguments type that has the - /// applied. This is done to avoid the situation - /// where the user wanted to use a generated parser, but is accidentally using reflection - /// instead. - /// - /// - /// If you need to support using arguments classes that may have the - /// attribute, for example when using commands from an external assembly that you don't - /// control, set this property to to avoid the exception. Note that - /// you will not get the performance benefits of the generated code in this case. + /// 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 CreateParser and Parse methods on the command line + /// arguments type, the generated parser is used regardless of the value of this property. /// /// - public bool AllowReflectionWithGeneratedParser { get; set; } = AllowReflectionWithGeneratedParserDefault; + public bool ForceReflection { get; set; } = ForceReflectionDefault; // Used by the tests so we can get coverage of the default options path while not causing // exceptions. - internal static bool AllowReflectionWithGeneratedParserDefault { get; set; } + internal static bool ForceReflectionDefault { get; set; } /// /// Gets or sets the to use to create usage help. diff --git a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs index 97f79feb..c80a5e82 100644 --- a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs @@ -49,16 +49,6 @@ public override CommandLineParser CreateParser() throw new InvalidOperationException(Properties.Resources.NoParserForCustomParsingCommand); } - if (Attribute.IsDefined(CommandType, typeof(GeneratedParserAttribute))) - { - var method = CommandType.GetMethod("CreateParser", BindingFlags.Public | BindingFlags.Static); - if (method != null) - { - var parameters = new object[] { Manager.Options }; - return (CommandLineParser)(method.Invoke(null, parameters) ?? throw new InvalidOperationException("TODO")); - } - } - return new CommandLineParser(CommandType, Manager.Options); } From 30c0a6032352c263dda9d8ff5069befc40461b4c Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 12 Jun 2023 11:22:08 -0700 Subject: [PATCH 119/234] Add IsPosix convenience option. --- .../CommandOptionsTest.cs | 97 +++++++++++++++++++ .../ParseOptionsAttributeTest.cs | 63 ++++++++++++ .../ParseOptionsTest.cs | 80 +++++++++++++++ .../Commands/CommandOptions.cs | 60 ++++++++++++ src/Ookii.CommandLine/ParseOptions.cs | 60 ++++++++++++ .../ParseOptionsAttribute.cs | 58 +++++++++++ .../StringComparisonExtensions.cs | 3 + 7 files changed, 421 insertions(+) create mode 100644 src/Ookii.CommandLine.Tests/CommandOptionsTest.cs create mode 100644 src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs create mode 100644 src/Ookii.CommandLine.Tests/ParseOptionsTest.cs diff --git a/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs b/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs new file mode 100644 index 00000000..ca1a99a1 --- /dev/null +++ b/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs @@ -0,0 +1,97 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Terminal; +using System; + +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.IsNull(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.NameValueSeparator); + Assert.AreEqual(UsageHelpRequest.Full, 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/ParseOptionsAttributeTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs new file mode 100644 index 00000000..06f4367a --- /dev/null +++ b/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs @@ -0,0 +1,63 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Terminal; +using System; + +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.AreEqual(':', options.NameValueSeparator); + 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..0d8dc19e --- /dev/null +++ b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs @@ -0,0 +1,80 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Terminal; +using System; + +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.IsNull(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.NameValueSeparator); + Assert.AreEqual(UsageHelpRequest.Full, options.ShowUsageOnError); + Assert.IsNotNull(options.StringProvider); + Assert.IsNotNull(options.UsageWriter); + Assert.IsNull(options.UseErrorColor); + Assert.IsNull(options.ValueDescriptionTransform); + Assert.AreEqual(TextFormat.ForegroundYellow, options.WarningColor); + } + + [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); + } +} diff --git a/src/Ookii.CommandLine/Commands/CommandOptions.cs b/src/Ookii.CommandLine/Commands/CommandOptions.cs index 81feefd8..aa7d1ac1 100644 --- a/src/Ookii.CommandLine/Commands/CommandOptions.cs +++ b/src/Ookii.CommandLine/Commands/CommandOptions.cs @@ -9,6 +9,66 @@ namespace Ookii.CommandLine.Commands /// public class CommandOptions : ParseOptions { + /// + /// 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 and command names are case + /// sensitive, and argument names, command 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 , + /// 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 and + /// 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 + { + 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 set the type of string comparison to use for argument names. /// diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index d5cbc353..5b3d5ec4 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -53,6 +53,66 @@ public class ParseOptions /// 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"). + /// + /// + /// 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 virtual bool IsPosix + { + get => Mode == ParsingMode.LongShort && (ArgumentNameComparison?.IsCaseSensitive() ?? false) && + ArgumentNameTransform == NameTransform.DashCase && ValueDescriptionTransform == NameTransform.DashCase; + set + { + 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. diff --git a/src/Ookii.CommandLine/ParseOptionsAttribute.cs b/src/Ookii.CommandLine/ParseOptionsAttribute.cs index 793a3f07..07d2d046 100644 --- a/src/Ookii.CommandLine/ParseOptionsAttribute.cs +++ b/src/Ookii.CommandLine/ParseOptionsAttribute.cs @@ -44,6 +44,64 @@ public class ParseOptionsAttribute : Attribute /// 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"). + /// + /// + /// 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. 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 virtual bool IsPosix + { + 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. diff --git a/src/Ookii.CommandLine/StringComparisonExtensions.cs b/src/Ookii.CommandLine/StringComparisonExtensions.cs index e508b1a6..3bbf25d4 100644 --- a/src/Ookii.CommandLine/StringComparisonExtensions.cs +++ b/src/Ookii.CommandLine/StringComparisonExtensions.cs @@ -21,4 +21,7 @@ public static StringComparer GetComparer(this StringComparison comparison) return StringComparer.FromComparison(comparison); #endif } + + public static bool IsCaseSensitive(this StringComparison comparison) + => comparison is StringComparison.Ordinal or StringComparison.InvariantCulture or StringComparison.CurrentCulture; } From 82a15ec954c2705bd2ab28a085a975824e834bd6 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 12 Jun 2023 14:15:27 -0700 Subject: [PATCH 120/234] Add test for merging options. --- .../ParseOptionsTest.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs index 0d8dc19e..bd4f1092 100644 --- a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs +++ b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Ookii.CommandLine.Terminal; using System; +using System.Linq; namespace Ookii.CommandLine.Tests; @@ -77,4 +78,51 @@ public void TestIsPosix() 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.AreEqual(':', options.NameValueSeparator); + 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); + } } From aeb43c47ddcdd4649b58c8b22df472687eac8bb7 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 12 Jun 2023 15:52:19 -0700 Subject: [PATCH 121/234] Moved default values for options into ParseOptions class. --- .../ParseOptionsTest.cs | 14 ++ src/Ookii.CommandLine/CommandLineArgument.cs | 12 +- src/Ookii.CommandLine/CommandLineParser.cs | 22 +-- .../Commands/ParentCommand.cs | 2 +- src/Ookii.CommandLine/ParseOptions.cs | 128 +++++++++++++++++- src/Ookii.CommandLine/UsageWriter.cs | 4 +- 6 files changed, 161 insertions(+), 21 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs index bd4f1092..b5ac07c0 100644 --- a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs +++ b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs @@ -42,6 +42,20 @@ public void TestConstructor() 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); + Assert.AreEqual(':', options.NameValueSeparatorOrDefault); + Assert.AreEqual(NameTransform.None, options.ValueDescriptionTransformOrDefault); } [TestMethod] diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index d548687e..a56e4f8e 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1190,7 +1190,7 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, IEnumerable? shortAliasAttributes, IEnumerable? validationAttributes) { - var argumentName = DetermineArgumentName(attribute.ArgumentName, memberName, parser.Options.ArgumentNameTransform); + var argumentName = DetermineArgumentName(attribute.ArgumentName, memberName, parser.Options.ArgumentNameTransformOrDefault); return new ArgumentInfo() { Parser = parser, @@ -1236,7 +1236,7 @@ private string DetermineValueDescription(Type? type = null) } var typeName = DetermineValueDescriptionForType(type ?? ElementType); - return Parser.Options.ValueDescriptionTransform?.Apply(typeName) ?? typeName; + return Parser.Options.ValueDescriptionTransformOrDefault.Apply(typeName); } private static string GetFriendlyTypeName(Type type) @@ -1399,7 +1399,7 @@ internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParse throw new ArgumentNullException(nameof(parser)); } - var argumentName = DetermineArgumentName(null, parser.StringProvider.AutomaticHelpName(), parser.Options.ArgumentNameTransform); + 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) ?? @@ -1422,7 +1422,7 @@ internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParse throw new ArgumentNullException(nameof(parser)); } - var argumentName = DetermineArgumentName(null, parser.StringProvider.AutomaticVersionName(), parser.Options.ArgumentNameTransform); + var argumentName = DetermineArgumentName(null, parser.StringProvider.AutomaticVersionName(), parser.Options.ArgumentNameTransformOrDefault); if (parser.GetArgument(argumentName) != null) { return null; @@ -1569,14 +1569,14 @@ private static CancelMode AutomaticVersion(CommandLineParser parser) return CancelMode.Abort; } - internal static string DetermineArgumentName(string? explicitName, string memberName, NameTransform? transform) + internal static string DetermineArgumentName(string? explicitName, string memberName, NameTransform transform) { if (explicitName != null) { return explicitName; } - return transform?.Apply(memberName) ?? memberName; + return transform.Apply(memberName); } private string? GetDefaultValueDescription(Type? type) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 807a6f46..df94049a 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -354,14 +354,14 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null _parseOptions.Merge(optionsAttribute); } - _mode = _parseOptions.Mode ?? default; - var comparison = _parseOptions.ArgumentNameComparison ?? StringComparison.OrdinalIgnoreCase; + _mode = _parseOptions.ModeOrDefault; + var comparison = _parseOptions.ArgumentNameComparisonOrDefault; ArgumentNameComparison = comparison; _argumentNamePrefixes = DetermineArgumentNamePrefixes(_parseOptions); var prefixInfos = _argumentNamePrefixes.Select(p => new PrefixInfo { Prefix = p, Short = true }); if (_mode == ParsingMode.LongShort) { - _longArgumentNamePrefix = _parseOptions.LongArgumentNamePrefix ?? DefaultLongArgumentNamePrefix; + _longArgumentNamePrefix = _parseOptions.LongArgumentNamePrefixOrDefault; if (string.IsNullOrWhiteSpace(_longArgumentNamePrefix)) { throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefix, nameof(options)); @@ -507,7 +507,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - public CultureInfo Culture => _parseOptions.Culture ?? CultureInfo.InvariantCulture; + public CultureInfo Culture => _parseOptions.CultureOrDefault; /// /// Gets a value indicating whether duplicate arguments are allowed. @@ -535,7 +535,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - public bool AllowDuplicateArguments => (_parseOptions.DuplicateArguments ?? default) != ErrorMode.Error; + public bool AllowDuplicateArguments => _parseOptions.DuplicateArgumentsOrDefault != ErrorMode.Error; /// /// Gets value indicating whether the value of an argument may be in a separate @@ -578,7 +578,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - public bool AllowWhiteSpaceValueSeparator => _parseOptions.AllowWhiteSpaceValueSeparator ?? true; + public bool AllowWhiteSpaceValueSeparator => _parseOptions.AllowWhiteSpaceValueSeparatorOrDefault; /// /// Gets or sets the character used to separate the name and the value of an argument. @@ -612,7 +612,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - public char NameValueSeparator => _parseOptions.NameValueSeparator ?? DefaultNameValueSeparator; + public char NameValueSeparator => _parseOptions.NameValueSeparatorOrDefault; /// /// Gets or sets a value that indicates whether usage help should be displayed if the @@ -985,7 +985,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = public object? ParseWithErrorHandling(ReadOnlyMemory args) { EventHandler? handler = null; - if (_parseOptions.DuplicateArguments == ErrorMode.Warning) + if (_parseOptions.DuplicateArgumentsOrDefault == ErrorMode.Warning) { handler = (sender, e) => { @@ -1354,7 +1354,7 @@ private int DetermineMemberArguments() private void DetermineAutomaticArguments() { - bool autoHelp = Options.AutoHelpArgument ?? true; + bool autoHelp = Options.AutoHelpArgumentOrDefault; if (autoHelp) { var (argument, created) = CommandLineArgument.CreateAutomaticHelp(this); @@ -1367,7 +1367,7 @@ private void DetermineAutomaticArguments() HelpArgument = argument; } - bool autoVersion = Options.AutoVersionArgument ?? true; + bool autoVersion = Options.AutoVersionArgumentOrDefault; if (autoVersion && !_provider.IsCommand) { var argument = CommandLineArgument.CreateAutomaticVersion(this); @@ -1631,7 +1631,7 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri if (argument == null && !_argumentsByName.TryGetValue(argumentName, out argument)) { - if (Options.AutoPrefixAliases ?? true) + if (Options.AutoPrefixAliasesOrDefault) { argument = GetArgumentByNamePrefix(argumentName.Span); } diff --git a/src/Ookii.CommandLine/Commands/ParentCommand.cs b/src/Ookii.CommandLine/Commands/ParentCommand.cs index 41e96e2b..2fb79d87 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommand.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommand.cs @@ -83,7 +83,7 @@ public void Parse(ReadOnlyMemory args, CommandManager manager) var parser = info.CreateParser(); EventHandler? handler = null; - if (parser.Options.DuplicateArguments == ErrorMode.Warning) + if (parser.Options.DuplicateArgumentsOrDefault == ErrorMode.Warning) { handler = (sender, e) => { diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 5b3d5ec4..00deeba2 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -35,6 +35,16 @@ public class ParseOptions /// public CultureInfo? Culture { get; set; } + /// + /// Gets the culture used to convert command line argument values from their string + /// representation to the argument type. + /// + /// + /// The value of the property, or + /// if that property is . + /// + public CultureInfo CultureOrDefault => Culture ?? CultureInfo.InvariantCulture; + /// /// Gets or sets a value that indicates the command line argument parsing rules to use. /// @@ -53,6 +63,15 @@ public class ParseOptions /// 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; + /// /// Gets or sets a value that indicates whether the options follow POSIX conventions. /// @@ -92,7 +111,7 @@ public class ParseOptions /// public virtual bool IsPosix { - get => Mode == ParsingMode.LongShort && (ArgumentNameComparison?.IsCaseSensitive() ?? false) && + get => Mode == ParsingMode.LongShort && ArgumentNameComparisonOrDefault.IsCaseSensitive() && ArgumentNameTransform == NameTransform.DashCase && ValueDescriptionTransform == NameTransform.DashCase; set { @@ -143,6 +162,16 @@ public virtual bool IsPosix /// 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. /// @@ -168,6 +197,16 @@ public virtual bool IsPosix 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. /// @@ -195,6 +234,16 @@ public virtual bool IsPosix /// 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. /// @@ -215,6 +264,16 @@ public virtual bool IsPosix /// 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. @@ -260,6 +319,15 @@ public virtual bool IsPosix /// 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. /// @@ -279,6 +347,16 @@ public virtual bool IsPosix /// 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 character used to separate the name and the value of an argument. /// @@ -313,6 +391,16 @@ public virtual bool IsPosix /// public char? NameValueSeparator { get; set; } + /// + /// Gets the character used to separate the name and the value of an argument. + /// + /// + /// The value of the property, or the value of the + /// constant if that property is + /// . + /// + public char NameValueSeparatorOrDefault => NameValueSeparator ?? CommandLineParser.DefaultNameValueSeparator; + /// /// Gets or sets a value that indicates a help argument will be automatically added. /// @@ -348,6 +436,15 @@ public virtual bool IsPosix /// 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. /// @@ -381,6 +478,16 @@ public virtual bool IsPosix /// 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 whether unique prefixes of an argument are automatically /// used as aliases. @@ -417,6 +524,16 @@ public virtual bool IsPosix /// 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. /// @@ -577,6 +694,15 @@ public LocalizedStringProvider StringProvider /// 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 diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index f92932ea..c4818f8f 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -1700,10 +1700,10 @@ protected virtual void WriteCommandListUsageCore() if (IncludeCommandHelpInstruction) { var prefix = CommandManager.Options.Mode == ParsingMode.LongShort - ? (CommandManager.Options.LongArgumentNamePrefix ?? CommandLineParser.DefaultLongArgumentNamePrefix) + ? (CommandManager.Options.LongArgumentNamePrefixOrDefault) : (CommandManager.Options.ArgumentNamePrefixes?.FirstOrDefault() ?? CommandLineParser.GetDefaultArgumentNamePrefixes()[0]); - var transform = CommandManager.Options.ArgumentNameTransform ?? NameTransform.None; + var transform = CommandManager.Options.ArgumentNameTransformOrDefault; var argumentName = transform.Apply(CommandManager.Options.StringProvider.AutomaticHelpName()); Writer.Indent = 0; From e79c156d7a99415045bd3a1389053501b7762b15 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 12 Jun 2023 16:35:11 -0700 Subject: [PATCH 122/234] Support multiple name/value separators. --- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 2 +- .../CommandLineParserTest.cs | 37 +++++-- .../CommandOptionsTest.cs | 2 +- .../ParseOptionsAttributeTest.cs | 2 +- .../ParseOptionsTest.cs | 6 +- src/Ookii.CommandLine/CommandLineParser.cs | 102 +++++++++--------- src/Ookii.CommandLine/ParseOptions.cs | 27 ++--- .../ParseOptionsAttribute.cs | 24 +++-- .../Properties/Resources.Designer.cs | 9 ++ .../Properties/Resources.resx | 3 + src/Ookii.CommandLine/StringExtensions.cs | 6 ++ src/Ookii.CommandLine/UsageWriter.cs | 6 +- 12 files changed, 134 insertions(+), 92 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index ba9d2010..12f6b543 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -202,7 +202,7 @@ partial class CancelArguments ArgumentNamePrefixes = new[] { "--", "-" }, LongArgumentNamePrefix = "---", CaseSensitive = true, - NameValueSeparator = '=', + NameValueSeparators = new[] { '=' }, AutoHelpArgument = false)] partial class ParseOptionsArguments { diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 90bc8044..22295032 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -217,27 +217,44 @@ public void ParseTestMultiValueSeparator(ProviderKind kind) public void ParseTestNameValueSeparator(ProviderKind kind) { var target = CreateParser(kind); - Assert.AreEqual(CommandLineParser.DefaultNameValueSeparator, target.NameValueSeparator); - SimpleArguments args = target.Parse(new[] { "-Argument1:test", "-Argument2:foo:bar" }); + 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" }, + new[] { "-Argument1>test" }, CommandLineArgumentErrorCategory.UnknownArgument, - "Argument1=test", + "Argument1>test", remainingArgumentCount: 1); - target.Options.NameValueSeparator = '='; - args = target.Parse(new[] { "-Argument1=test", "-Argument2=foo=bar" }); + var options = new ParseOptions() + { + 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); + 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] @@ -635,7 +652,7 @@ public void TestParseOptionsAttribute(ProviderKind kind) var parser = CreateParser(kind); Assert.IsFalse(parser.AllowWhiteSpaceValueSeparator); Assert.IsTrue(parser.AllowDuplicateArguments); - Assert.AreEqual('=', parser.NameValueSeparator); + CollectionAssert.AreEquivalent(new[] { '=' }, parser.NameValueSeparators); Assert.AreEqual(ParsingMode.LongShort, parser.Mode); CollectionAssert.AreEqual(new[] { "--", "-" }, parser.ArgumentNamePrefixes); Assert.AreEqual("---", parser.LongArgumentNamePrefix); @@ -652,7 +669,7 @@ public void TestParseOptionsAttribute(ProviderKind kind) ArgumentNameComparison = StringComparison.OrdinalIgnoreCase, AllowWhiteSpaceValueSeparator = true, DuplicateArguments = ErrorMode.Error, - NameValueSeparator = ';', + NameValueSeparators = new[] { ';' }, ArgumentNamePrefixes = new[] { "+" }, AutoHelpArgument = true, }; @@ -660,7 +677,7 @@ public void TestParseOptionsAttribute(ProviderKind kind) parser = CreateParser(kind, options); Assert.IsTrue(parser.AllowWhiteSpaceValueSeparator); Assert.IsFalse(parser.AllowDuplicateArguments); - Assert.AreEqual(';', parser.NameValueSeparator); + CollectionAssert.AreEquivalent(new[] { ';' }, parser.NameValueSeparators); Assert.AreEqual(ParsingMode.Default, parser.Mode); CollectionAssert.AreEqual(new[] { "+" }, parser.ArgumentNamePrefixes); Assert.IsNull(parser.LongArgumentNamePrefix); diff --git a/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs b/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs index ca1a99a1..ecd6914e 100644 --- a/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs @@ -36,7 +36,7 @@ public void TestConstructor() Assert.IsFalse(options.IsPosix); Assert.IsNull(options.LongArgumentNamePrefix); Assert.IsNull(options.Mode); - Assert.IsNull(options.NameValueSeparator); + Assert.IsNull(options.NameValueSeparators); Assert.AreEqual(UsageHelpRequest.Full, options.ShowUsageOnError); Assert.IsNotNull(options.StringProvider); Assert.IsNotNull(options.UsageWriter); diff --git a/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs index 06f4367a..878d01ab 100644 --- a/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs +++ b/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs @@ -22,7 +22,7 @@ public void TestConstructor() Assert.IsFalse(options.IsPosix); Assert.IsNull(options.LongArgumentNamePrefix); Assert.AreEqual(ParsingMode.Default, options.Mode); - Assert.AreEqual(':', options.NameValueSeparator); + Assert.IsNull(options.NameValueSeparators); Assert.AreEqual(NameTransform.None, options.ValueDescriptionTransform); } diff --git a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs index b5ac07c0..a1c8955e 100644 --- a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs +++ b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs @@ -35,7 +35,7 @@ public void TestConstructor() Assert.IsFalse(options.IsPosix); Assert.IsNull(options.LongArgumentNamePrefix); Assert.IsNull(options.Mode); - Assert.IsNull(options.NameValueSeparator); + Assert.IsNull(options.NameValueSeparators); Assert.AreEqual(UsageHelpRequest.Full, options.ShowUsageOnError); Assert.IsNotNull(options.StringProvider); Assert.IsNotNull(options.UsageWriter); @@ -54,7 +54,7 @@ public void TestConstructor() Assert.AreEqual(ErrorMode.Error, options.DuplicateArgumentsOrDefault); Assert.AreEqual("--", options.LongArgumentNamePrefixOrDefault); Assert.AreEqual(ParsingMode.Default, options.ModeOrDefault); - Assert.AreEqual(':', options.NameValueSeparatorOrDefault); + CollectionAssert.AreEqual(new[] { ':', '=' }, options.NameValueSeparatorsOrDefault.ToArray()); Assert.AreEqual(NameTransform.None, options.ValueDescriptionTransformOrDefault); } @@ -110,7 +110,7 @@ public void TestMerge() Assert.IsFalse(options.IsPosix); Assert.IsNull(options.LongArgumentNamePrefix); Assert.AreEqual(ParsingMode.Default, options.Mode); - Assert.AreEqual(':', options.NameValueSeparator); + Assert.IsNull(options.NameValueSeparators); Assert.AreEqual(NameTransform.None, options.ValueDescriptionTransform); options = new ParseOptions(); diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index df94049a..0a3eafd3 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -200,23 +200,13 @@ private struct PrefixInfo private readonly PrefixInfo[] _sortedPrefixes; private readonly string[] _argumentNamePrefixes; private readonly string? _longArgumentNamePrefix; + private readonly char[] _nameValueSeparators; private ReadOnlyCollection? _argumentsReadOnlyWrapper; private ReadOnlyCollection? _argumentNamePrefixesReadOnlyWrapper; + private ReadOnlyCollection? _nameValueSeparatorsReadOnlyWrapper; private List? _requiredPropertyArguments; - /// - /// 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 /// . @@ -358,6 +348,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null 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) { @@ -485,11 +476,11 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// 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. + /// , 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; @@ -542,29 +533,29 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// 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 + /// if names and values can be in separate arguments; 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 character - /// specified in the property or by using white space (i.e. + /// 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. /// /// - /// If the property is , only the character - /// specified in the property is allowed to separate the value from the name. + /// 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 character specified in the property is allowed + /// 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 @@ -581,38 +572,20 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null public bool AllowWhiteSpaceValueSeparator => _parseOptions.AllowWhiteSpaceValueSeparatorOrDefault; /// - /// Gets or sets the character used to separate the name and the value of an argument. + /// Gets 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. The default value is the - /// constant, a colon (:). + /// The characters used to separate the name and the value of an argument. /// /// /// - /// 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.NameValueSeparatorOrDefault; + /// + /// + public ReadOnlyCollection NameValueSeparators => _nameValueSeparatorsReadOnlyWrapper ??= new(_nameValueSeparators); /// /// Gets or sets a value that indicates whether usage help should be displayed if the @@ -1258,6 +1231,19 @@ public static string[] GetDefaultArgumentNamePrefixes() : new[] { "-" }; } + /// + /// Gets the default character 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 char[] GetDefaultNameValueSeparators() => new[] { ':', '=' }; + /// /// Raises the event. /// @@ -1337,6 +1323,24 @@ private static string[] DetermineArgumentNamePrefixes(ParseOptions options) } } + private static char[] DetermineNameValueSeparators(ParseOptions options) + { + if (options.NameValueSeparators == null) + { + return GetDefaultNameValueSeparators(); + } + else + { + var result = options.NameValueSeparators.ToArray(); + if (result.Length == 0) + { + throw new ArgumentException(Properties.Resources.EmptyNameValueSeparators, nameof(options)); + } + + return result; + } + } + private int DetermineMemberArguments() { int additionalPositionalArgumentCount = 0; @@ -1381,7 +1385,7 @@ private void DetermineAutomaticArguments() private void AddNamedArgument(CommandLineArgument argument) { - if (argument.ArgumentName.Contains(NameValueSeparator)) + if (_nameValueSeparators.Any(separator => argument.ArgumentName.Contains(separator))) { throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.ArgumentNameContainsSeparatorFormat, argument.ArgumentName)); } @@ -1611,7 +1615,7 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri private (CancelMode, int, CommandLineArgument?) ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo prefix) { - var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitOnce(NameValueSeparator); + var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitFirstOfAny(_nameValueSeparators); CancelMode cancelParsing; CommandLineArgument? argument = null; diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 00deeba2..0b8fabef 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -333,7 +333,7 @@ public virtual bool IsPosix /// /// /// if white space is allowed to separate an argument name and its - /// value; if only the is allowed, + /// 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 . @@ -358,18 +358,19 @@ public virtual bool IsPosix public bool AllowWhiteSpaceValueSeparatorOrDefault => AllowWhiteSpaceValueSeparator ?? true; /// - /// Gets or sets the character used to separate the name and the value of an argument. + /// 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 - /// constant, a colon (:). The default value is . + /// is not present, the values returned by the + /// method, which are a colon (:) and an equals sign (=). 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. + /// 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, @@ -386,20 +387,20 @@ public virtual bool IsPosix /// /// /// If not , this property overrides the value of the - /// property. + /// property. /// /// - public char? NameValueSeparator { get; set; } + public IEnumerable? NameValueSeparators { get; set; } /// - /// Gets the character used to separate the name and the value of an argument. + /// Gets the characters used to separate the name and the value of an argument. /// /// - /// The value of the property, or the value of the - /// constant if that property is + /// The value of the property, or the return value of the + /// method if that property is /// . /// - public char NameValueSeparatorOrDefault => NameValueSeparator ?? CommandLineParser.DefaultNameValueSeparator; + public IEnumerable NameValueSeparatorsOrDefault => NameValueSeparators ?? CommandLineParser.GetDefaultNameValueSeparators(); /// /// Gets or sets a value that indicates a help argument will be automatically added. @@ -772,7 +773,7 @@ public void Merge(ParseOptionsAttribute attribute) ArgumentNameComparison ??= attribute.GetStringComparison(); DuplicateArguments ??= attribute.DuplicateArguments; AllowWhiteSpaceValueSeparator ??= attribute.AllowWhiteSpaceValueSeparator; - NameValueSeparator ??= attribute.NameValueSeparator; + NameValueSeparators ??= attribute.NameValueSeparators; AutoHelpArgument ??= attribute.AutoHelpArgument; AutoVersionArgument ??= attribute.AutoVersionArgument; AutoPrefixAliases ??= attribute.AutoPrefixAliases; diff --git a/src/Ookii.CommandLine/ParseOptionsAttribute.cs b/src/Ookii.CommandLine/ParseOptionsAttribute.cs index 07d2d046..3a6cc008 100644 --- a/src/Ookii.CommandLine/ParseOptionsAttribute.cs +++ b/src/Ookii.CommandLine/ParseOptionsAttribute.cs @@ -230,8 +230,8 @@ public virtual bool IsPosix /// /// /// if white space is allowed to separate an argument name and its - /// value; if only the value from - /// is allowed. The default value is . + /// value; if only the values from + /// are allowed. The default value is . /// /// /// @@ -246,13 +246,15 @@ public virtual bool IsPosix /// 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 (:). + /// 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 . /// /// /// - /// 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. + /// 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, @@ -261,19 +263,19 @@ public virtual bool IsPosix /// case the value is "foo:bar"). /// /// - /// Do not pick a whitespace character as the separator. Doing this only works if the + /// 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 whitespace + /// property to control whether white space /// is allowed as a separator. /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// - /// - public char NameValueSeparator { get; set; } = CommandLineParser.DefaultNameValueSeparator; + /// + public char[]? NameValueSeparators { get; set; } /// /// Gets or sets a value that indicates a help argument will be automatically added. diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 1fbe3ea6..d94d4da5 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -312,6 +312,15 @@ 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}'.. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index 9c0349ff..5beaef7e 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -408,4 +408,7 @@ The specified TypeConverter cannot converter from a string. + + You must specify at least one name/value separator. + \ No newline at end of file diff --git a/src/Ookii.CommandLine/StringExtensions.cs b/src/Ookii.CommandLine/StringExtensions.cs index 767f5d40..e02c4b03 100644 --- a/src/Ookii.CommandLine/StringExtensions.cs +++ b/src/Ookii.CommandLine/StringExtensions.cs @@ -10,6 +10,12 @@ public static (ReadOnlyMemory, ReadOnlyMemory?) SplitOnce(this ReadO 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 StringSpanTuple SplitOnce(this ReadOnlySpan value, ReadOnlySpan separator, out bool hasSeparator) { var index = value.IndexOf(separator); diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index c4818f8f..eb82a6e0 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -364,8 +364,8 @@ public bool IncludeExecutableExtension 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 + /// Gets or sets a value indicating whether white space, rather than the first item of the + /// property, is used to separate /// arguments and their values in the command line syntax. /// /// @@ -1031,7 +1031,7 @@ protected virtual void WriteArgumentSyntax(CommandLineArgument argument) char? separator = argument.Parser.AllowWhiteSpaceValueSeparator && UseWhiteSpaceValueSeparator ? null - : argument.Parser.NameValueSeparator; + : argument.Parser.NameValueSeparators[0]; if (argument.Position == null) { From 41b3a6cf65fe4e52d3d57698deb3b931befec95f Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 12 Jun 2023 17:13:08 -0700 Subject: [PATCH 123/234] List possible values if enum value conversion fails. --- .../CommandLineParserTest.cs | 6 +++--- src/Ookii.CommandLine/Conversion/EnumConverter.cs | 14 ++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 22295032..93b8c5a6 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -997,12 +997,12 @@ public void TestValidation(ProviderKind kind) CollectionAssert.AreEqual(new[] { "foo", "bar", "baz", "ban" }, result.Arg4); // Enum validator - CheckThrows(parser, new[] { "-Day", "foo" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(FormatException), remainingArgumentCount: 2); + 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(FormatException), remainingArgumentCount: 2); + CheckThrows(parser, new[] { "-Day", "" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day", typeof(ArgumentException), remainingArgumentCount: 2); result = parser.Parse(new[] { "-Day", "1" }); Assert.AreEqual(DayOfWeek.Monday, result.Day); - CheckThrows(parser, new[] { "-Day2", "foo" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day2", typeof(FormatException), remainingArgumentCount: 2); + CheckThrows(parser, new[] { "-Day2", "foo" }, CommandLineArgumentErrorCategory.ArgumentValueConversion, "Day2", typeof(ArgumentException), remainingArgumentCount: 2); CheckThrows(parser, new[] { "-Day2", "9" }, CommandLineArgumentErrorCategory.ValidationFailed, "Day2", remainingArgumentCount: 2); result = parser.Parse(new[] { "-Day2", "1" }); Assert.AreEqual(DayOfWeek.Monday, result.Day2); diff --git a/src/Ookii.CommandLine/Conversion/EnumConverter.cs b/src/Ookii.CommandLine/Conversion/EnumConverter.cs index b324c8e6..b30f75c6 100644 --- a/src/Ookii.CommandLine/Conversion/EnumConverter.cs +++ b/src/Ookii.CommandLine/Conversion/EnumConverter.cs @@ -30,11 +30,11 @@ public EnumConverter(Type enumType) } catch (ArgumentException ex) { - throw new FormatException(ex.Message, ex); + throw CreateException(value, ex, argument); } catch (OverflowException ex) { - throw new FormatException(ex.Message, ex); + throw CreateException(value, ex, argument); } } @@ -48,12 +48,18 @@ public EnumConverter(Type enumType) } catch (ArgumentException ex) { - throw new FormatException(ex.Message, ex); + throw CreateException(value.ToString(), ex, argument); } catch (OverflowException ex) { - throw new FormatException(ex.Message, 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); + } } From 0de12b2aa58ca229c2f05c051c9f29f49d081088 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 12 Jun 2023 17:17:51 -0700 Subject: [PATCH 124/234] EnumConverter XML comment updates. --- src/Ookii.CommandLine/Conversion/EnumConverter.cs | 6 ++++++ .../Validation/ValidateEnumValueAttribute.cs | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine/Conversion/EnumConverter.cs b/src/Ookii.CommandLine/Conversion/EnumConverter.cs index b30f75c6..a8c4e5b7 100644 --- a/src/Ookii.CommandLine/Conversion/EnumConverter.cs +++ b/src/Ookii.CommandLine/Conversion/EnumConverter.cs @@ -6,6 +6,12 @@ namespace Ookii.CommandLine.Conversion; /// /// A converter for arguments with enumeration values. /// +/// +/// +/// If conversion fails, this converter will provide an error message that includes all the +/// allowed values for the enumeration. +/// +/// /// public class EnumConverter : ArgumentConverter { diff --git a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs index 65226c94..9af9e99b 100644 --- a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs @@ -63,8 +63,10 @@ public override bool IsValid(CommandLineArgument argument, object? value) /// /// /// - /// 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. + /// 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; } From 29bb4c4bf08e9df0ef1d7874225da9a1c28f7b90 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 12 Jun 2023 17:37:15 -0700 Subject: [PATCH 125/234] Use AssemblyTitleAttribute if no ApplicationFriendlyNameAttribute. --- .../Commands.cs | 5 ++++- .../Ookii.CommandLine.Tests.Commands.csproj | 1 + .../CommandLineParserTest.cs | 18 +++++++++++++++++- .../Ookii.CommandLine.Tests.csproj | 1 + src/Ookii.CommandLine/CommandLineParser.cs | 3 ++- .../Support/GeneratedArgumentProvider.cs | 5 ++++- .../Support/ReflectionArgumentProvider.cs | 3 ++- 7 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Ookii.CommandLine.Tests.Commands/Commands.cs b/src/Ookii.CommandLine.Tests.Commands/Commands.cs index 2c4bfda5..5d9458a2 100644 --- a/src/Ookii.CommandLine.Tests.Commands/Commands.cs +++ b/src/Ookii.CommandLine.Tests.Commands/Commands.cs @@ -3,8 +3,11 @@ namespace Ookii.CommandLine.Tests.Commands; +#pragma warning disable OCL0034 // Subcommands should have a description. + [Command("external")] -public class ExternalCommand : ICommand +[GeneratedParser] +public partial class ExternalCommand : 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 index 0c137a6f..bafddff5 100644 --- a/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj +++ b/src/Ookii.CommandLine.Tests.Commands/Ookii.CommandLine.Tests.Commands.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 93b8c5a6..11e4290f 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1,6 +1,7 @@ // Copyright (c) Sven Groot (Ookii.org) using Microsoft.VisualStudio.TestTools.UnitTesting; using Ookii.CommandLine.Support; +using Ookii.CommandLine.Tests.Commands; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -40,7 +41,7 @@ public void ConstructorEmptyArgumentsTest(ProviderKind kind) 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("Ookii.CommandLine Unit Tests", target.ApplicationFriendlyName); Assert.AreEqual(string.Empty, target.Description); Assert.AreEqual(2, target.Arguments.Count); using var args = target.Arguments.GetEnumerator(); @@ -1267,6 +1268,21 @@ public void TestAutoPrefixAliases(ProviderKind kind) CheckThrows(parser, new[] { "-pro", "foo", "-Po", "5", "-e" }, CommandLineArgumentErrorCategory.UnknownArgument, "pro", remainingArgumentCount: 5); } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestApplicationFriendlyName(ProviderKind kind) + { + CommandLineParser parser = CreateParser(kind); + Assert.AreEqual("Friendly name", parser.ApplicationFriendlyName); + + // Default to assembly title if no friendly name. + parser = CreateParser(kind); + Assert.AreEqual("Ookii.CommandLine Unit Tests", parser.ApplicationFriendlyName); + + parser = CreateParser(kind); + Assert.AreEqual("Ookii.CommandLine.Tests.Commands", parser.ApplicationFriendlyName); + } + private class ExpectedArgument { public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind.SingleValue) diff --git a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj index 3c3122fa..8c671298 100644 --- a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj +++ b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj @@ -3,6 +3,7 @@ net7.0;net6.0;net48 disable + Ookii.CommandLine Unit Tests Tests for Ookii.CommandLine. false 11.0 diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 0a3eafd3..5f58a5eb 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -443,7 +443,8 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// 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. + /// 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" diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs index 5a788bcb..8c9d4047 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -50,7 +51,9 @@ protected GeneratedArgumentProvider(Type argumentsType, /// public override string ApplicationFriendlyName - => _friendlyNameAttribute?.Name ?? ArgumentsType.Assembly.GetName().Name ?? string.Empty; + => _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/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs index 961d55df..12a2ac53 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -30,7 +30,8 @@ public override string ApplicationFriendlyName var attribute = ArgumentsType.GetCustomAttribute() ?? ArgumentsType.Assembly.GetCustomAttribute(); - return attribute?.Name ?? ArgumentsType.Assembly.GetName().Name ?? string.Empty; + return attribute?.Name ?? ArgumentsType.Assembly.GetCustomAttribute()?.Title ?? + ArgumentsType.Assembly.GetName().Name ?? string.Empty; } } From b493701b2878019e38f50ed4148233acff25138b Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 12 Jun 2023 18:06:18 -0700 Subject: [PATCH 126/234] Rewrote first part. --- docs/SourceGeneration.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/SourceGeneration.md b/docs/SourceGeneration.md index 0094d024..a496ea4e 100644 --- a/docs/SourceGeneration.md +++ b/docs/SourceGeneration.md @@ -1,10 +1,18 @@ # Source generation -Ookii.CommandLine includes a source generator that can be used to generate a `CommandLineParser` -for an arguments type, or a `CommandManager` for the commands in an assembly, at compile time. The -source generator will generate C# code that creates those classes using information about your -arguments or command types available during compilation, rather than determining that information at -runtime using reflection. +Ookii.CommandLine has two ways by which it can determine which arguments are available. + +- Reflection will inspect the members of the arguments type at runtime, check for the + `CommandLineParserAttribute`, and provide that information to the `CommandLineParser`. This was + the only method available before version 4.0, and is still used if the `GeneratedParserAttribute` + is not present. +- Source generation will perform the same inspection at compile time, generating C# code that will + provide the required information to the `CommandLineParser` without runtime overhead. This is + used as of version 4.0 when the `GeneratedParserAttribute` is present. + +The same also applies to [subcommands](Subcommands.md). The `CommandManager` class uses runtime +reflection by default to discover the subcommands in an assembly, and source generation is available +with the `GeneratedCommandManagerAttribute` to do that same work at compile time. Using source generation has several benefits: @@ -19,11 +27,10 @@ Using source generation has several benefits: source generation is not used, the way Ookii.CommandLine uses reflection prevents trimming entirely. - Specify [default values using property initializers](#default-values-using-property-initializers). -- Improved performance. Benchmarks show that instantiating a `CommandLineParser` using a - generated parser is up to thirty times faster than using reflection. However, since we're still - talking about microseconds, this is unlikely to matter that much to a typical application. +- Improved performance; benchmarks show that instantiating a `CommandLineParser` using a + generated parser is up to thirty times faster than using reflection. -A few restrictions apply to projects that use Ookii.ComandLine's source generation: +A few restrictions apply to projects that use Ookii.CommandLine's source generation: - The project must a C# project (other languages are not supported), using C# version 8 or later. - The project must be built using a recent version of the .Net SDK (TODO: Exact version). From 5bd6290c6bf68eec2efa5d81dc004ca63f504a36 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 13 Jun 2023 11:20:40 -0700 Subject: [PATCH 127/234] Source generator .Net 6.0 SDK support, and better language version handling. --- docs/SourceGenerationDiagnostics.md | 20 +++++++++++++++++++ .../Diagnostics.cs | 9 +++++++++ .../Ookii.CommandLine.Generator.csproj | 3 ++- .../ParserGenerator.cs | 18 ++++++++++++----- .../ParserIncrementalGenerator.cs | 12 +++++++++-- .../Properties/Resources.Designer.cs | 18 +++++++++++++++++ .../Properties/Resources.resx | 6 ++++++ 7 files changed, 78 insertions(+), 8 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 0ad7daaa..7684f6c9 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -359,6 +359,26 @@ partial class Arguments } ``` +### OCL0037 + +Source generation with the `GeneratedParserAttribute` or `CommandManagerAttribute` requires at +least C# language version 8.0. + +The code that is generated by Ookii.CommandLine's [source generation](SourceGeneration.md) uses +language features that are only available in C# 8.0. Use the `` configuration property +to specify the language version if you are targeting a framework that does not use a new enough +version by default. + +```xml + + 8.0 + +``` + +If you cannot change the language version, remove the `GeneratedParserAttribute` or +`CommandManagerAttribute` and use the `CommandLineParser` class, `CommandLineParser.Parse()` +methods, or `CommandManager` class directly to use reflection instead of source generation. + ## Warnings ### OCL0016 diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 7883ea9b..c8176cbc 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -341,6 +341,15 @@ public static Diagnostic IgnoredFriendlyNameAttribute(ISymbol symbol, AttributeD attribute.GetLocation(), symbol.ToDisplayString()); + public static Diagnostic UnsupportedLanguageVersion(ISymbol symbol, string attributeName) => CreateDiagnostic( + "OCL0037", + nameof(Resources.UnsupportedLanguageVersionTitle), + nameof(Resources.UnsupportedLanguageVersionMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString(), + attributeName); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj b/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj index 66d0c8b5..2df9996d 100644 --- a/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj +++ b/src/Ookii.CommandLine.Generator/Ookii.CommandLine.Generator.csproj @@ -17,7 +17,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 2108499c..70ebbf36 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -44,10 +44,12 @@ private struct PositionalArgumentInfo private readonly SourceBuilder _builder; private readonly ConverterGenerator _converterGenerator; private readonly CommandGenerator _commandGenerator; + private readonly LanguageVersion _languageVersion; private Dictionary? _positions; private List? _positionalArguments; - public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumentsClass, TypeHelper typeHelper, ConverterGenerator converterGenerator, CommandGenerator commandGenerator) + public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumentsClass, TypeHelper typeHelper, + ConverterGenerator converterGenerator, CommandGenerator commandGenerator, LanguageVersion languageVersion) { _typeHelper = typeHelper; _compilation = typeHelper.Compilation; @@ -56,11 +58,15 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen _builder = new(argumentsClass.ContainingNamespace); _converterGenerator = converterGenerator; _commandGenerator = commandGenerator; + _languageVersion = languageVersion; } - public static string? Generate(SourceProductionContext context, INamedTypeSymbol argumentsClass, TypeHelper typeHelper, ConverterGenerator converterGenerator, CommandGenerator commandGenerator) + 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); + var generator = new ParserGenerator(context, argumentsClass, typeHelper, converterGenerator, commandGenerator, + languageVersion); + return generator.Generate(); } @@ -106,7 +112,9 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen } _builder.AppendLine($"partial class {_argumentsClass.Name}"); - if (_typeHelper.IParser != null) + // 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) { @@ -148,7 +156,7 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen } } - _builder.AppendLine($"public static Ookii.CommandLine.CommandLineParser<{_argumentsClass.Name}> CreateParser(Ookii.CommandLine.ParseOptions? options = null) => new(new OokiiCommandLineArgumentProvider(), options);"); + _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); diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index e15b72a2..dc8a2351 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -30,7 +30,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Where(static c => c != null); var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); - context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Left, source.Right!, spc)); } @@ -49,6 +48,7 @@ private static void Execute(Compilation compilation, ImmutableArray 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) { @@ -75,6 +75,12 @@ private static void Execute(Compilation compilation, ImmutableArray ? 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)); @@ -105,7 +111,9 @@ private static void Execute(Compilation compilation, ImmutableArray continue; } - var source = ParserGenerator.Generate(context, symbol, typeHelper, converterGenerator, commandGenerator); + 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)); diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 01c56011..53733946 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -743,5 +743,23 @@ internal static string UnknownAttributeTitle { return ResourceManager.GetString("UnknownAttributeTitle", 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 index 63fe4f63..4ad366f2 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -345,4 +345,10 @@ Unknown attribute will be ignored. + + 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 From baac9738f4794c37bb72dad894e63cd4f9243419 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 13 Jun 2023 13:29:18 -0700 Subject: [PATCH 128/234] Support more expression types for property initializers. --- docs/SourceGenerationDiagnostics.md | 44 +++++++++++++++++++ .../Diagnostics.cs | 8 ++++ src/Ookii.CommandLine.Generator/Extensions.cs | 3 +- .../ParserGenerator.cs | 39 +++++++++++++++- .../Properties/Resources.Designer.cs | 18 ++++++++ .../Properties/Resources.resx | 6 +++ src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 27 +++++++++++- .../CommandLineParserTest.cs | 12 ++++- 8 files changed, 151 insertions(+), 6 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 7684f6c9..b8518ba0 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -832,3 +832,47 @@ Instead, the attribute should be applied to the assembly: ```csharp [assembly: ApplicationFriendlyName("My Application")] ``` + +### OCL0038 + +The initial value of a property will not be included in the usage help, because it uses an +expression type that is not supported by the source generator. Supported expression types are +literals, enumeration values, constants, and null-forgiving expressions containing any of those +expression types. + +For example, `5`, `"value"`, `DayOfWeek.Tuesday`, `int.MaxValue` and `default!` are all supported +expressions for property initializers. + +Any other type of expression, such as a method invocation or constructing a new object, is not +supported and will not be included in the usage help. + +For example, the following code triggers this warning: + +```csharp +// WARNING: ApplicationFriendlyName is ignored for commands. +[GeneratedParser] +partial class Arguments +{ + // WARNING: Method call for property initializer is not supported for the usage help. + [CommandLineAttribute] + public string? Argument { get; set; } = GetDefaultValue(); + + private static int GetDefaultValue() + { + // omitted. + } +} +``` + +This will not affect the actual value of the argument, since the property will not be set by the +`CommandLineParser` if the `CommandLineArgumentAttribute.DefaultValue` property is null. Therefore, +you can safely suppress this warning and include the relevant explanation of the default value in +the property's description manually, if desired. + +To avoid this warning, use one of the supported expression types, or use the +`CommandLineArgumentAttribute.DefaultValue` property. This warning will not be emitted if the +`CommandLineArgumentAttribute.DefaultValue` property is not null, regardless of the initializer. + +Note that default values set by property initializers are only shown in the usage help if the +`GeneratedParserAttribute` is used. When reflection is used, only +`CommandLineArgumentAttribute.DefaultValue` is supported. diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index c8176cbc..3842190e 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -350,6 +350,14 @@ public static Diagnostic UnsupportedLanguageVersion(ISymbol symbol, string attri symbol.ToDisplayString(), attributeName); + public static Diagnostic UnsupportedInitializerSyntax(ISymbol symbol, Location location) => CreateDiagnostic( + "OCL0038", + nameof(Resources.UnsupportedInitializerSyntaxTitle), + nameof(Resources.UnsupportedInitializerSyntaxMessageFormat), + DiagnosticSeverity.Warning, + location, + symbol.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index cf1a6328..dffbd306 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -15,6 +15,7 @@ internal static class Extensions globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + memberOptions: SymbolDisplayMemberOptions.IncludeContainingType, miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers); public static bool DerivesFrom(this ITypeSymbol symbol, ITypeSymbol? baseClass) @@ -223,7 +224,7 @@ public static bool CheckType(this AttributeData data, ITypeSymbol? attributeType public static Location? GetLocation(this AttributeData attribute) => attribute.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span); - public static string ToQualifiedName(this ITypeSymbol symbol) + public static string ToQualifiedName(this ISymbol symbol) { return symbol.ToDisplayString(QualifiedFormat); } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 70ebbf36..0f11c8b3 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -870,7 +870,42 @@ private void CheckIgnoredDictionaryAttribute(ISymbol member, bool isDictionary, private string? GetInitializerValue(IPropertySymbol symbol) { var syntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(_context.CancellationToken) as PropertyDeclarationSyntax; - var value = syntax?.Initializer?.Value as LiteralExpressionSyntax; - return value?.Token.ToFullString(); + 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/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 53733946..43617ce0 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -744,6 +744,24 @@ internal static string UnknownAttributeTitle { } } + /// + /// 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, or constant. 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.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 4ad366f2..3c6ce948 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -345,6 +345,12 @@ Unknown attribute will be ignored. + + The initial value of the property '{0}' will not be included in the usage help because it is not a literal expression, enum value, or constant. 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. diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 12f6b543..a08ca0a9 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -9,7 +9,7 @@ using System.Net; // We deliberately have some properties and methods that cause warnings, so disable those. -#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029,OCL0033 +#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029,OCL0033,OCL0038 namespace Ookii.CommandLine.Tests; @@ -632,6 +632,31 @@ partial class InitializerDefaultValueArguments [CommandLineArgument] public int Arg3 { get; set; } = int.MaxValue; + + [CommandLineArgument] + public DayOfWeek Arg4 { get; set; } = DayOfWeek.Tuesday; + + [CommandLineArgument] + public int Arg5 { get; set; } = Value; + + [CommandLineArgument] + public int Arg6 { get; set; } = GetValue(); + + [CommandLineArgument] + public int Arg7 { get; set; } = default; + +#nullable enable + [CommandLineArgument] + public string? Arg8 { get; set; } = default!; + + [CommandLineArgument] + public string? Arg9 { get; set; } = null!; +#nullable disable + + private const int Value = 47; + + public static int GetValue() => 42; + } [GeneratedParser] diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 11e4290f..acaaadcc 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1234,8 +1234,16 @@ public void TestInitializerDefaultValues() var parser = InitializerDefaultValueArguments.CreateParser(); Assert.AreEqual("foo\tbar\"", parser.GetArgument("Arg1").DefaultValue); Assert.AreEqual(5.5f, parser.GetArgument("Arg2").DefaultValue); - // Arg3's default value can't be used because it's not a literal. - Assert.IsNull(parser.GetArgument("Arg3").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); } [TestMethod] From c52998b1ecfe27967e4ba33491eb4451c912b4b8 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 13 Jun 2023 14:47:08 -0700 Subject: [PATCH 129/234] Automatic positional arguments. --- docs/SourceGenerationDiagnostics.md | 31 +++++++++++-- .../CommandLineArgumentAttributeInfo.cs | 7 +++ .../Diagnostics.cs | 8 ++++ .../ParserGenerator.cs | 43 ++++++++++++++++--- .../Properties/Resources.Designer.cs | 18 ++++++++ .../Properties/Resources.resx | 6 +++ src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 25 +++++++++++ .../CommandLineParserTest.cs | 26 +++++++++++ .../CommandLineArgumentAttribute.cs | 42 ++++++++++++++++-- .../Properties/Resources.Designer.cs | 11 ++++- .../Properties/Resources.resx | 5 ++- .../Support/GeneratedArgument.cs | 8 ++++ .../Support/ReflectionArgument.cs | 5 +++ 13 files changed, 220 insertions(+), 15 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index b8518ba0..abd8fba3 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -349,13 +349,11 @@ For example, the following code triggers this error: ```csharp [GeneratedParser] -[ParseOptions(Mode = ParsingMode.LongShort)] partial class Arguments { // ERROR: No long or short name (IsShort is false by default). [CommandLineAttribute(IsLong = false)] - [ArgumentConverter("MyNamespace.MyConverter")] - public CustomType? Argument { get; set; } + public string? Argument { get; set; } } ``` @@ -379,6 +377,33 @@ If you cannot change the language version, remove the `GeneratedParserAttribute` `CommandManagerAttribute` and use the `CommandLineParser` class, `CommandLineParser.Parse()` methods, or `CommandManager` class directly to use reflection instead of source generation. +### OCL0038 + +Positional arguments using an explicit position with the `CommandLineArgumentAttribute.Position` +property, and those using a position derived from their member ordering using the +`CommandLineArgumentAttribute.IsPositional` property cannot be mixed. Note that this includes any +arguments defined in a base class. + +For example, the following code triggers this error: + +```csharp +// ERROR: Argument1 uses automatic positioning, and Argument2 uses an explicit position. +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute(IsPositional = true)] + public string? Argument1 { get; set; } + + [CommandLineAttribute(Position = 0)] + public string? Argument2 { get; set; } +} +``` + +Please switch all arguments to use either explicit or automatic positions. + +Note that using `CommandLineArgumentAttribute.IsPositional` without an explicit position does not +work without the `GeneratedParserAttribute`. + ## Warnings ### OCL0016 diff --git a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs index fdbdc8f7..0f375544 100644 --- a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs +++ b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs @@ -5,6 +5,7 @@ namespace Ookii.CommandLine.Generator; internal class CommandLineArgumentAttributeInfo { private readonly bool _isShort; + private readonly bool _isPositional; public CommandLineArgumentAttributeInfo(AttributeData data) { @@ -35,6 +36,10 @@ public CommandLineArgumentAttributeInfo(AttributeData data) break; + case nameof(IsPositional): + _isPositional = (bool)named.Value.Value!; + break; + case nameof(IsShort): _isShort = (bool)named.Value.Value!; ExplicitIsShort = _isShort; @@ -63,6 +68,8 @@ public CommandLineArgumentAttributeInfo(AttributeData data) public int? Position { get; } + public bool IsPositional => _isPositional || Position != null; + public object? DefaultValue { get; } public bool IsShort => _isShort || ShortName != '\0'; diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 3842190e..fbfa9b75 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -358,6 +358,14 @@ public static Diagnostic UnsupportedInitializerSyntax(ISymbol symbol, Location l location, symbol.ToDisplayString()); + public static Diagnostic MixedImplicitExplicitPositions(ISymbol symbol) => CreateDiagnostic( + "OCL0039", + nameof(Resources.MixedImplicitExplicitPositionsTitle), + nameof(Resources.MixedImplicitExplicitPositionsMessageFormat), + DiagnosticSeverity.Error, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 0f11c8b3..5192f89c 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -45,6 +45,8 @@ private struct PositionalArgumentInfo private readonly ConverterGenerator _converterGenerator; private readonly CommandGenerator _commandGenerator; private readonly LanguageVersion _languageVersion; + private bool _hasImplicitPositions; + private int _nextImplicitPosition; private Dictionary? _positions; private List? _positionalArguments; @@ -197,20 +199,28 @@ private bool GenerateProvider(ArgumentsClassAttributes attributes, bool isComman _builder.AppendLine("public override System.Collections.Generic.IEnumerable GetArguments(Ookii.CommandLine.CommandLineParser parser)"); _builder.OpenBlock(); - var current = _argumentsClass; List<(string, string, string)>? requiredProperties = null; var hasError = false; - while (current != null && current.SpecialType != SpecialType.System_Object) + + // 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) { - foreach (var member in current.GetMembers()) + argumentTypes.Push(current); + } + + foreach (var type in argumentTypes) + { + foreach (var member in type.GetMembers()) { if (!GenerateArgument(member, ref requiredProperties)) { hasError = true; } } - - current = current.BaseType; } if (!VerifyPositionalArgumentRules()) @@ -543,10 +553,14 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> } } - _builder.CloseArgumentList(); - _builder.AppendLine(); if (argumentInfo.Position is int position) { + if (_hasImplicitPositions) + { + _context.ReportDiagnostic(Diagnostics.MixedImplicitExplicitPositions(_argumentsClass)); + return false; + } + _positions ??= new(); if (_positions.TryGetValue(position, out string name)) { @@ -566,6 +580,21 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> 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. diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 43617ce0..c2887a08 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -510,6 +510,24 @@ internal static string IsShortIgnoredTitle { } } + /// + /// 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}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 3c6ce948..c5bed80b 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -267,6 +267,12 @@ 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}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter. diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index a08ca0a9..16b6f323 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -672,3 +672,28 @@ partial class AutoPrefixAliasesArguments [Alias("Prefix")] public bool EnablePrefix { get; set; } } + +class AutoPositionArgumentsBase +{ + [CommandLineArgument(IsPositional = true, IsRequired = true)] + public string BaseArg1 { get; set; } + + [CommandLineArgument(IsPositional = true)] + public int BaseArg2 { get; set; } + + [CommandLineArgument] + public int BaseArg3 { get; set; } +} + +[GeneratedParser] +partial class AutoPositionArguments : AutoPositionArgumentsBase +{ + [CommandLineArgument(IsPositional = true)] + public string Arg1 { get; set; } + + [CommandLineArgument(IsPositional = true)] + public int Arg2 { get; set; } + + [CommandLineArgument] + public int Arg3 { get; set; } +} diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index acaaadcc..0356342f 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1291,6 +1291,32 @@ public void TestApplicationFriendlyName(ProviderKind kind) Assert.AreEqual("Ookii.CommandLine.Tests.Commands", parser.ApplicationFriendlyName); } + [TestMethod] + public void TestAutoPosition() + { + var parser = AutoPositionArguments.CreateParser(); + VerifyArguments(parser.Arguments, new[] + { + 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 + { + parser = new CommandLineParser(); + Debug.Fail("Expected exception not thrown."); + } + catch (NotSupportedException) + { + } + } + private class ExpectedArgument { public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind.SingleValue) diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index 506cd860..43c53cc6 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -60,6 +60,7 @@ 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. @@ -205,9 +206,13 @@ public bool IsShort /// 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. + /// 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, @@ -217,6 +222,37 @@ public bool IsShort /// 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 , 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. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index d94d4da5..9607c4d0 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -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.. /// @@ -376,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 { diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index 5beaef7e..ed473b6e 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -157,7 +157,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. @@ -411,4 +411,7 @@ 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. + \ No newline at end of file diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 835c9aa7..4f672f09 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -46,6 +46,7 @@ private GeneratedArgument(ArgumentInfo info, Action? setPropert /// /// /// + /// /// /// /// @@ -72,6 +73,7 @@ public static GeneratedArgument Create(CommandLineParser parser, ArgumentConverter converter, bool allowsNull, string defaultValueDescription, + int? position = null, string? defaultKeyDescription = null, bool requiredProperty = false, object? alternateDefaultValue = null, @@ -89,6 +91,12 @@ public static GeneratedArgument Create(CommandLineParser parser, 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, multiValueSeparatorAttribute, descriptionAttribute, valueDescriptionAttribute, allowDuplicateDictionaryKeys, keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index 0a64c00a..a0f04d63 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -130,6 +130,11 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo 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(); From 524c879b8643ccd708c025699b869d4470b71d85 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 13 Jun 2023 14:49:03 -0700 Subject: [PATCH 130/234] Remove TODOs for ctor arguments. --- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 24 --------------- .../CommandLineParserNullableTest.cs | 15 ---------- .../CommandLineParserTest.cs | 8 ----- .../NullableArgumentTypes.cs | 29 ------------------- src/Ookii.CommandLine/CommandLineArgument.cs | 1 - 5 files changed, 77 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 16b6f323..67b8e4b1 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -516,30 +516,6 @@ public InjectionArguments(CommandLineParser parser) public int Arg { get; set; } } -// TODO: Test with new ctor argument style. -//class InjectionMixedArguments -//{ -// private readonly CommandLineParser _parser; -// private readonly int _arg1; -// private readonly int _arg2; - -// public InjectionMixedArguments(int arg1, CommandLineParser parser, int arg2) -// { -// _arg1 = arg1; -// _parser = parser; -// _arg2 = arg2; -// } - -// public CommandLineParser Parser => _parser; - -// public int Arg1 => _arg1; - -// public int Arg2 => _arg2; - -// [CommandLineArgument] -// public int Arg3 { get; set; } -//} - struct StructWithParseCulture { public int Value { get; set; } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs index ce643bf6..bbcf8f51 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -58,21 +58,6 @@ public void TestAllowNull(ProviderKind kind) Assert.IsTrue(parser.GetArgument("NullableValueIDictionary")!.AllowNull); } - [TestMethod] - [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] - public void TestNonNullableConstructor(ProviderKind kind) - { - // TODO: Update for new ctor arguments style. - var parser = CommandLineParserTest.CreateParser(kind); - 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); - } - [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] public void TestNonNullableProperties(ProviderKind kind) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 0356342f..65c97e67 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1128,14 +1128,6 @@ public void TestInjection(ProviderKind kind) var result = parser.Parse(new[] { "-Arg", "1" }); Assert.AreSame(parser, result.Parser); Assert.AreEqual(1, result.Arg); - - // TODO: - //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); } [TestMethod] diff --git a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs index 15b54850..9c2aa7f4 100644 --- a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs @@ -43,35 +43,6 @@ class NullReturningIntConverter : ArgumentConverter [GeneratedParser] partial class NullableArguments { - // TODO: Put back with new ctor approach. - //public TestArguments( - // [ArgumentConverter(typeof(NullReturningStringConverter))] string? constructorNullable, - // [ArgumentConverter(typeof(NullReturningStringConverter))] string constructorNonNullable, - // [ArgumentConverter(typeof(NullReturningIntConverter))] int constructorValueType, - // [ArgumentConverter(typeof(NullReturningIntConverter))] int? constructorNullableValueType) - //{ - // ConstructorNullable = constructorNullable; - // ConstructorNonNullable = constructorNonNullable; - // ConstructorValueType = constructorValueType; - // ConstructorNullableValueType = constructorNullableValueType; - //} - - [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"; diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index a56e4f8e..4641d8d0 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1440,7 +1440,6 @@ internal void ApplyPropertyValue(object target) { // Do nothing for method-based values, or for required properties if the provider is not // using reflection. - // TODO: Handle new style constructor parameters. if (Kind == ArgumentKind.Method || (IsRequiredProperty && _parser.ProviderKind != ProviderKind.Reflection)) { return; From c4fe2c215181e28ab79ede13ae76df803b842316 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 13 Jun 2023 15:25:22 -0700 Subject: [PATCH 131/234] Plumb cancellation token for LineWrappingTextWriter.WriteAsync. --- .../NullableArgumentTypes.cs | 16 + src/Ookii.CommandLine/Convert-SyncMethod.ps1 | 7 +- .../LineWrappingTextWriter.Async.cs | 479 +++++++++--------- .../LineWrappingTextWriter.cs | 15 +- src/Ookii.CommandLine/RingBuffer.Async.cs | 59 ++- src/Ookii.CommandLine/RingBuffer.cs | 287 +++++------ src/Ookii.CommandLine/StringSpan.Async.cs | 5 +- 7 files changed, 455 insertions(+), 413 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs index 9c2aa7f4..5101c2ad 100644 --- a/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/NullableArgumentTypes.cs @@ -43,6 +43,22 @@ class NullReturningIntConverter : ArgumentConverter [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"; 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/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..512dcf08 100644 --- a/src/Ookii.CommandLine/LineWrappingTextWriter.cs +++ b/src/Ookii.CommandLine/LineWrappingTextWriter.cs @@ -657,8 +657,7 @@ public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken c return Task.FromCanceled(cancellationToken); } - // TODO: Use cancellation token if possible. - _asyncWriteTask = WriteCoreAsync(buffer); + _asyncWriteTask = WriteCoreAsync(buffer, cancellationToken); return _asyncWriteTask; } @@ -715,6 +714,7 @@ public override async Task WriteLineAsync(ReadOnlyMemory buffer, Cancellat /// 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. /// /// @@ -736,9 +736,9 @@ public override async Task WriteLineAsync(ReadOnlyMemory buffer, Cancellat /// set to . /// /// - public Task FlushAsync(bool insertNewLine) + public Task FlushAsync(bool insertNewLine, CancellationToken cancellationToken = default) { - var task = FlushCoreAsync(insertNewLine); + var task = FlushCoreAsync(insertNewLine, cancellationToken); _asyncWriteTask = task; return task; } @@ -763,6 +763,7 @@ public Task FlushAsync(bool insertNewLine) /// /// 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. /// @@ -778,9 +779,9 @@ public Task FlushAsync(bool insertNewLine) /// the output position is simply reset to the beginning of the line without writing anything to the base writer. /// /// - public Task ResetIndentAsync() + public Task ResetIndentAsync(CancellationToken cancellationToken = default) { - var task = ResetIndentCoreAsync(); + var task = ResetIndentCoreAsync(cancellationToken); _asyncWriteTask = task; return task; } @@ -844,6 +845,8 @@ protected override void Dispose(bool disposing) private partial void ResetIndentCore(); + private static partial void WriteBlankLine(TextWriter writer); + private static int GetLineLengthForConsole() { try diff --git a/src/Ookii.CommandLine/RingBuffer.Async.cs b/src/Ookii.CommandLine/RingBuffer.Async.cs index 22dfac21..9edf14a8 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 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..fc167e39 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 partial void WriteHelper(TextWriter writer, char[] buffer, int index, int length); } diff --git a/src/Ookii.CommandLine/StringSpan.Async.cs b/src/Ookii.CommandLine/StringSpan.Async.cs index 8dd324a2..82ae61dc 100644 --- a/src/Ookii.CommandLine/StringSpan.Async.cs +++ b/src/Ookii.CommandLine/StringSpan.Async.cs @@ -3,6 +3,7 @@ using Ookii.CommandLine.Terminal; using System; using System.IO; +using System.Threading; using System.Threading.Tasks; namespace Ookii.CommandLine @@ -11,10 +12,10 @@ namespace Ookii.CommandLine internal static partial class StringSpanExtensions { - public static async Task WriteToAsync(this ReadOnlyMemory self, TextWriter writer) + public static async Task WriteToAsync(this ReadOnlyMemory self, TextWriter writer, CancellationToken cancellationToken) { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await writer.WriteAsync(self); + await writer.WriteAsync(self, cancellationToken); #else await writer.WriteAsync(self.ToString()); #endif From 3a1b2b014a63f3dab026cc449a8e1cf146e40de9 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 13 Jun 2023 15:27:38 -0700 Subject: [PATCH 132/234] Use lower case anchor. --- docs/SourceGenerationDiagnostics.md | 2 +- src/Ookii.CommandLine.Generator/Diagnostics.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index abd8fba3..49ef0c56 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -858,7 +858,7 @@ Instead, the attribute should be applied to the assembly: [assembly: ApplicationFriendlyName("My Application")] ``` -### OCL0038 +### OCL0039 The initial value of a property will not be included in the usage help, because it uses an expression type that is not supported by the source generator. Supported expression types are diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index fbfa9b75..f20ffb45 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -375,6 +375,6 @@ private static Diagnostic CreateDiagnostic(string id, string titleResource, stri Category, severity, isEnabledByDefault: true, - helpLinkUri: $"https://www.ookii.org/Link/CommandLineGeneratorError#{id}"), + helpLinkUri: $"https://www.ookii.org/Link/CommandLineGeneratorError#{id.ToLowerInvariant()}"), location, messageArgs); } From 207f583dc71216c71ad29b68fc797bdb271c6c2e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 13 Jun 2023 15:41:25 -0700 Subject: [PATCH 133/234] Updated messages and URLs for RequiresUnreferencedCodeAttribute. --- src/Ookii.CommandLine/CommandLineParser.cs | 12 +++++++----- src/Ookii.CommandLine/CommandLineParserGeneric.cs | 2 +- src/Ookii.CommandLine/Commands/CommandInfo.cs | 6 +++--- src/Ookii.CommandLine/Commands/CommandManager.cs | 6 +++--- .../Commands/ParentCommandAttribute.cs | 2 +- .../Conversion/KeyValuePairConverter.cs | 2 +- src/Ookii.CommandLine/Support/ReflectionArgument.cs | 2 +- .../Support/ReflectionArgumentProvider.cs | 2 +- .../Support/ReflectionCommandInfo.cs | 2 +- .../Support/ReflectionCommandProvider.cs | 2 +- src/Ookii.CommandLine/TypeHelper.cs | 2 +- 11 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 5f58a5eb..5a394be1 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -259,6 +259,8 @@ private struct PrefixInfo /// 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. @@ -295,7 +297,7 @@ private struct PrefixInfo /// /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] + [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) @@ -1052,7 +1054,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] + [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 @@ -1094,7 +1096,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] + [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = UnreferencedCodeHelpUrl)] #endif public static T? Parse(string[] args, int index, ParseOptions? options = null) where T : class @@ -1132,7 +1134,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] + [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 @@ -1774,7 +1776,7 @@ private CommandLineArgument GetShortArgumentOrThrow(char shortName) } #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] + [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = UnreferencedCodeHelpUrl)] #endif private static ArgumentProvider GetArgumentProvider(Type type, ParseOptions? options) { diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index 088ff19a..7c7cd298 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -42,7 +42,7 @@ public class CommandLineParser : CommandLineParser /// /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining arguments via reflection. Use the GeneratedArgumentsParserAttribute instead.")] + [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) diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index 092bfdc6..aed2e44c 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -333,7 +333,7 @@ public bool MatchesPrefix(string prefix) /// if was not a command. /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandManagerAttribute instead.")] + [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); @@ -356,7 +356,7 @@ public bool MatchesPrefix(string prefix) /// A class with information about the command. /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandManagerAttribute instead.")] + [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); @@ -373,7 +373,7 @@ public static CommandInfo Create(Type commandType, CommandManager manager) /// is . /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandManagerAttribute instead.")] + [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] #endif public static bool IsCommand(Type commandType) => ReflectionCommandInfo.GetCommandAttribute(commandType) != null; diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index 1728d0a4..fbf23a15 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -73,7 +73,7 @@ public class CommandManager /// /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] + [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) @@ -123,7 +123,7 @@ protected CommandManager(CommandProvider provider, CommandOptions? options = nul /// /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] + [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) @@ -155,7 +155,7 @@ public CommandManager(Assembly assembly, CommandOptions? options = null) /// /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] + [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) diff --git a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs index 8395c748..de31cd94 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs @@ -67,7 +67,7 @@ public ParentCommandAttribute(Type parentCommandType) public string ParentCommandTypeName { get; } #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining commands via reflection. Use the GeneratedCommandManagerAttribute instead.")] + [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/Conversion/KeyValuePairConverter.cs b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs index 4f5c3ce2..594e9357 100644 --- a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs +++ b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs @@ -73,7 +73,7 @@ public KeyValuePairConverter(ArgumentConverter keyConverter, ArgumentConverter v /// Initializes a new instance of the class. /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining converter types via reflection.")] + [RequiresUnreferencedCode("Key and value converters cannot be statically determined.")] #endif public KeyValuePairConverter() : this(typeof(TKey).GetStringConverter(null), typeof(TValue).GetStringConverter(null), null, true) diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index a0f04d63..349650a0 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -14,7 +14,7 @@ namespace Ookii.CommandLine.Support; #if NET6_0_OR_GREATER -[RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] +[RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] #endif internal class ReflectionArgument : CommandLineArgument { diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs index 12a2ac53..70cf58fe 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -12,7 +12,7 @@ namespace Ookii.CommandLine.Support; #if NET6_0_OR_GREATER -[RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] +[RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] #endif internal class ReflectionArgumentProvider : ArgumentProvider { diff --git a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs index c80a5e82..b7f44147 100644 --- a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs @@ -14,7 +14,7 @@ namespace Ookii.CommandLine.Support; #if NET6_0_OR_GREATER -[RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] +[RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] #endif internal class ReflectionCommandInfo : CommandInfo { diff --git a/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs b/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs index 90a33239..9ed1b1f9 100644 --- a/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionCommandProvider.cs @@ -9,7 +9,7 @@ namespace Ookii.CommandLine.Support; #if NET6_0_OR_GREATER -[RequiresUnreferencedCode("Trimming is not possible when determining commands using reflection. Use the GeneratedCommandManagerAttribute instead.")] +[RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] #endif internal class ReflectionCommandProvider : CommandProvider { diff --git a/src/Ookii.CommandLine/TypeHelper.cs b/src/Ookii.CommandLine/TypeHelper.cs index 58b9747a..5163d5d9 100644 --- a/src/Ookii.CommandLine/TypeHelper.cs +++ b/src/Ookii.CommandLine/TypeHelper.cs @@ -89,7 +89,7 @@ public static bool ImplementsInterface( } #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Trimming cannot be used when determining the default converter via reflection.")] + [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) { From 724e766d6aa47aa50dc0ece3d22e31f5985b4f41 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 13 Jun 2023 15:57:39 -0700 Subject: [PATCH 134/234] Made sure all exceptions have messages. --- src/Ookii.CommandLine/CommandLineArgument.cs | 25 +++++++++---- .../Properties/Resources.Designer.cs | 36 +++++++++++++++++++ .../Properties/Resources.resx | 12 +++++++ .../Support/GeneratedArgument.cs | 6 ++-- .../Support/ReflectionArgument.cs | 6 ++-- .../Validation/ArgumentValidationAttribute.cs | 2 +- 6 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 4641d8d0..bb7d3e3c 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -67,7 +67,9 @@ public void ApplyValue(CommandLineArgument argument, object target) return; } - var list = (ICollection?)argument.GetProperty(target) ?? throw new InvalidOperationException(); + var list = (ICollection?)argument.GetProperty(target) + ?? throw new InvalidOperationException(Properties.Resources.NullPropertyValue); + list.Clear(); foreach (var value in _values) { @@ -107,7 +109,7 @@ public void ApplyValue(CommandLineArgument argument, object target) } var dictionary = (IDictionary?)argument.GetProperty(target) - ?? throw new InvalidOperationException(); + ?? throw new InvalidOperationException(Properties.Resources.NullPropertyValue); dictionary.Clear(); foreach (var pair in _dictionary) @@ -154,7 +156,7 @@ private class MethodValueHelper : IValueHelper public void ApplyValue(CommandLineArgument argument, object target) { - throw new InvalidOperationException(); + throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); } public CancelMode SetValue(CommandLineArgument argument, object? value) @@ -224,8 +226,12 @@ private static ArgumentInfo CreateInfo(CommandLineParser parser, string argument } protected override CancelMode CallMethod(object? value) => CancelMode.Abort; - protected override object? GetProperty(object target) => throw new InvalidOperationException(); - protected override void SetProperty(object target, object? value) => throw new InvalidOperationException(); + + protected override object? GetProperty(object target) + => throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); + + protected override void SetProperty(object target, object? value) + => throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); } private class VersionArgument : CommandLineArgument @@ -256,8 +262,13 @@ private static ArgumentInfo CreateInfo(CommandLineParser parser, string argument } protected override CancelMode CallMethod(object? value) => AutomaticVersion(Parser); - protected override object? GetProperty(object target) => throw new InvalidOperationException(); - protected override void SetProperty(object target, object? value) => throw new InvalidOperationException(); + + protected override object? GetProperty(object target) + => throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); + + protected override void SetProperty(object target, object? value) + => throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); + } internal struct ArgumentInfo diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 9607c4d0..f10df5e4 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -375,6 +375,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.. /// @@ -393,6 +402,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.. /// @@ -420,6 +438,15 @@ internal static string InvalidTypeConverter { } } + /// + /// Looks up a localized string similar to The IsSpanValid method must be overridden if CanValidateSpan is set to true.. + /// + internal static string IsSpanValidNotImplemented { + get { + return ResourceManager.GetString("IsSpanValidNotImplemented", resourceCulture); + } + } + /// /// Looks up a localized string similar to The 'minimum' and 'maximum' parameters cannot both be null.. /// @@ -546,6 +573,15 @@ internal static string NullArgumentValueFormat { } } + /// + /// Looks up a localized string similar to A read-only property for a multi-value or dictionary argument returned null.. + /// + internal static string NullPropertyValue { + get { + 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.. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index ed473b6e..465420f2 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -414,4 +414,16 @@ 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. + + + The IsSpanValid method must be overridden if CanValidateSpan is set to true. + + + A read-only property for a multi-value or dictionary argument returned null. + \ No newline at end of file diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 4f672f09..9343073f 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -124,7 +124,7 @@ protected override CancelMode CallMethod(object? value) { if (_callMethod == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException(Properties.Resources.InvalidMethodAccess); } return _callMethod(value, this.Parser); @@ -135,7 +135,7 @@ protected override CancelMode CallMethod(object? value) { if (_getProperty == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); } return _getProperty(target); @@ -146,7 +146,7 @@ protected override void SetProperty(object target, object? value) { if (_setProperty == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); } _setProperty(target, value); diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index 349650a0..1911c1e5 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -45,7 +45,7 @@ protected override void SetProperty(object target, object? value) { if (_property == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); } _property.SetValue(target, value); @@ -55,7 +55,7 @@ protected override void SetProperty(object target, object? value) { if (_property == null) { - throw new InvalidOperationException(); + throw new InvalidOperationException(Properties.Resources.InvalidPropertyAccess); } return _property.GetValue(target); @@ -65,7 +65,7 @@ protected override CancelMode CallMethod(object? value) { if (_method is not MethodArgumentInfo info) { - throw new InvalidOperationException(); + throw new InvalidOperationException(Properties.Resources.InvalidMethodAccess); } int parameterCount = (info.HasValueParameter ? 1 : 0) + (info.HasParserParameter ? 1 : 0); diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs index 9ae96486..d7e8a20f 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs @@ -175,7 +175,7 @@ public void ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) /// /// public virtual bool IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) - => throw new NotImplementedException(); + => throw new NotImplementedException(Properties.Resources.IsSpanValidNotImplemented); /// /// Gets the error message to display if validation failed. From 56254fa4972f2c29c0d7387171fc35a13e91e283 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 13 Jun 2023 16:50:32 -0700 Subject: [PATCH 135/234] Updated source generation docs. --- docs/SourceGeneration.md | 230 +++++++++++++++++++++------------------ 1 file changed, 127 insertions(+), 103 deletions(-) diff --git a/docs/SourceGeneration.md b/docs/SourceGeneration.md index a496ea4e..53385787 100644 --- a/docs/SourceGeneration.md +++ b/docs/SourceGeneration.md @@ -7,7 +7,7 @@ Ookii.CommandLine has two ways by which it can determine which arguments are ava the only method available before version 4.0, and is still used if the `GeneratedParserAttribute` is not present. - Source generation will perform the same inspection at compile time, generating C# code that will - provide the required information to the `CommandLineParser` without runtime overhead. This is + provide the required information to the `CommandLineParser` with less runtime overhead. This is used as of version 4.0 when the `GeneratedParserAttribute` is present. The same also applies to [subcommands](Subcommands.md). The `CommandManager` class uses runtime @@ -16,41 +16,36 @@ with the `GeneratedCommandManagerAttribute` to do that same work at compile time Using source generation has several benefits: -- Get [errors and warnings](SourceGenerationDiagnostics.md) at compile time for argument rule - violations (such as a required positional argument after an optional positional argument), ignored - options (such as setting a default value for a required attribute), and other problems (such as - using the same position number more than once, method arguments with the wrong signature, or using - the `CommandLineArgumentAttribute` on a private or read-only property). These would normally be - silently ignored or cause a runtime exception, but now you can catch problems during compilation. -- Allow your application to be - [trimmed](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options). When - source generation is not used, the way Ookii.CommandLine uses reflection prevents trimming - entirely. +- Get [errors and warnings](SourceGenerationDiagnostics.md) at compile time for many common mistakes, + which would cause a runtime exception or be silently ignored when using reflection. +- Use [automatic ordering](#automatic-ordering-of-positional-arguments) for positional arguments. - Specify [default values using property initializers](#default-values-using-property-initializers). +- Allow your application to be + [trimmed](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options). It's not + possible to statically determine what types are needed to determine arguments using reflection, + so trimming is not possible at all with reflection. - Improved performance; benchmarks show that instantiating a `CommandLineParser` using a generated parser is up to thirty times faster than using reflection. A few restrictions apply to projects that use Ookii.CommandLine's source generation: -- The project must a C# project (other languages are not supported), using C# version 8 or later. -- The project must be built using a recent version of the .Net SDK (TODO: Exact version). +- The project must be a C# project (other languages are not supported), using C# version 8 or later. + Other languages or older C# versions are not supported. +- The project must be built using using the .Net 6.0 SDK or a later version. - You can still target older runtimes supported by Ookii.CommandLine, down to .Net Framework 4.6, - but you must build the project using an SDK that supports the source generator, and set the - appropriate language version using the `` property in your project file. -- If you use the `ArgumentConverterAttribute`, you must use the constructor that takes a `Type` - instance. The constructor that takes a string is not supported. -- The arguments or command manager class may not be nested in another type. -- The arguments or command manager class may not have generic type parameters. + but you must build the project using the .Net 6.0 SDK or newer. +- If you use the `ArgumentConverterAttribute` or `ParentCommandAttribute`, you must use the + constructor that takes a `Type` instance. The constructor that takes a string is not supported. +- The generated arguments or command manager class may not be nested in another type. +- The generated arguments or command manager class may not have generic type parameters. -Generally, it's recommended to use source generation unless you cannot meet these requirements, or -you have another reason why you cannot use it. +Generally, it's recommended to use source generation unless you cannot meet these requirements. ## Generating a parser -Normally, the `CommandLineParser` class uses runtime reflection to determine the command line -arguments defined by an arguments class. To use source generation instead, use the -`GeneratedParserAttribute` attribute on your arguments class. You must also mark the class as -`partial`, because the source generator will add additional members to your class. +To use source generation to determine the command line arguments defined by a class, apply the +`GeneratedParserAttribute` attribute to that class. You must also mark the class as `partial`, +because the source generator will add additional members to your class. ```csharp [GeneratedParser] @@ -61,7 +56,7 @@ partial class Arguments } ``` -The source generator will inspect the members and attributes of the class, and generates C# code +The source generator will inspect the members and attributes of the class, and generate C# code that provides that information to a `CommandLineParser`, without needing to use reflection. While doing so, it checks whether your class violates any rules for defining arguments, and [emits warnings and errors](SourceGenerationDiagnostics.md) if it does. @@ -69,32 +64,27 @@ doing so, it checks whether your class violates any rules for defining arguments If any of the arguments has a type for which there is no built-in `ArgumentConverter` class, and the argument doesn't use the `ArgumentConverterAttribute`, the source generator will check whether the type supports any of the standard methods of [argument value conversion](Arguments.md#argument-value-conversion), -and if it does, it will generate an `ArgumentConverter` implementation for that type (without -source generation, conversion for these types would normally also use reflection), and uses it +and if it does, it will generate an `ArgumentConverter` implementation for that type, and uses it for the argument. -Generated `ArgumentConverter` classes are internal to your project, and placed in the `Ookii.CommandLine.Conversion.Generated` -namespace. The namespace can be customized using the `GeneratedConverterNamespaceAttribute` -attribute. +Generated `ArgumentConverter` classes are internal to your project, and placed in the +`Ookii.CommandLine.Conversion.Generated` namespace. The namespace can be customized using the +`GeneratedConverterNamespaceAttribute` attribute. -You can view any of the generated files using Visual Studio by looking under Dependencies, -Analyzers, Ookii.CommandLine.Generator in the Solution Explorer, or by setting the -`` property to true in your project file, in which case the generated -files will be placed under the `obj` folder of your project. +If you use Visual Studio, you can view the generated files by looking under Dependencies, +Analyzers, Ookii.CommandLine.Generator in the Solution Explorer. -### Using a generated parser +You can also set the `` property to true in your project file, in which +case the generated files will be placed under the `obj` folder of your project. -When using the `GeneratedParserAttribute`, you must *not* use the regular `CommandLineParser` or -`CommandLineParser` constructor, or the static `CommandLineParser.Parse()` methods. These will -still use reflection, even if a generated parser is available for a class. +### Using a generated parser -> By default, these constructors and methods will throw an exception if you try to use them with a -> class that has the `GeneratedParserAttribute`, to prevent accidentally using reflection when it -> was not intended. If for some reason you need to use reflection on a class that has that -> attribute, you can set the `ParseOptions.AllowReflectionWithGeneratedParser` property to `true`. +You can use the regular `CommandLineParser` or `CommandLineParser` constructors, or the static +`CommandLineParser.Parse()` methods, which will automatically use the generated argument +information if it is available. -Instead, you should use one of the methods that the source generator will add to your arguments -class (where `Arguments` is the name of your class): +For convenience, the source generator also adds the following methods to your arguments class (where +`Arguments` is the name of your class): ```csharp public static CommandLineParser CreateParser(ParseOptions? options = null); @@ -103,12 +93,16 @@ public static Arguments? Parse(ParseOptions? options = null); public static Arguments? Parse(string[] args, ParseOptions? options = null); -public static Arguments? Parse(string[] args, int index, ParseOptions? options = null); +public static Arguments? Parse(ReadOnlyMemory args, ParseOptions? options = null); ``` Use the `CreateParser()` method as an alternative to the `CommandLineParser` constructor, and the `Parse()` methods as an alternative to the static `CommandLineParser.Parse()` methods. +Generally, it's recommended to use these generated methods. If you want to trim your application, +you must use them, since the regular `CommandLineParser` constructor will still use reflection to +determine if generated argument information is present, and therefore still prohibits trimming. + So, if you had the following code before using source generation: ```csharp @@ -121,17 +115,64 @@ You would replace it with the following: var arguments = Arguments.Parse(); ``` -If your project targets .Net 7 or later, the generated class will implement the `IParserProvider` -and `IParser` interfaces, which define these methods. +Everything else remains the same. + +If your project targets .Net 7.0 or later, the generated class will implement the +`IParserProvider` and `IParser` interfaces, which define the generated methods. Generating the `Parse()` methods is optional, and can be disabled using the `GeneratedParserAttribute.GenerateParseMethods` property. The `CreateParser()` method is always generated. +### Automatic ordering of positional arguments + +When using the `GeneratedParserAttribute`, you do not have to specify explicit positions for +positional arguments. Instead, you can use the `CommandLineArgumentAttribute.IsPositional` +property to indicate which arguments are positional, and the order will be determined by the order +of the members that define the arguments. + +That means instead of this: + +```csharp +class Arguments +{ + [CommandLineArgument(Position = 0)] + public string? SomeArgument { get; set; } + + [CommandLineArgument(Position = 1)] + public int OtherArgument { get; set; } +} +``` + +You can now do this: + +```csharp +class Arguments +{ + [CommandLineArgument(IsPositional = true)] + public string? SomeArgument { get; set; } + + [CommandLineArgument(IsPositional = true)] + public int OtherArgument { get; set; } +} +``` + +This means you no longer have to be careful about ordering when adding new arguments, and don't +have to worry about accidentally using the same position more than once. + +If your class derives from a base class that defines positional arguments, those will come before +the arguments of the derived class. + +If you use automatic ordering, all positional arguments must use it. Mixing explicit positions and +automatic positions is not allowed. + +Using automatic ordering is not possible with reflection, because reflection does not guarantee it +will return the members of the class in any particular order. + ### Default values using property initializers -When using the source generation to create a command line parser, you can use property initializers -to specify the default value of an argument, and still have that value be used in the usage help. +When using the source generation, you can use property initializers to specify the default value of +an argument, and still have that value be used in the usage help. ```csharp [GeneratedParser] @@ -145,41 +186,51 @@ partial class Arguments } ``` -When using the reflection-based parser with the default constructors of the `CommandLineParser` -class, `Arg2` would have its value set to "foo" when omitted (since Ookii.CommandLine doesn't -assign the property if the argument is not specifies), but that default value would not be included -in the usage help, whereas `Arg1` does. +When using a reflection-based parser, `Arg2` would have its value set to "foo" when omitted (since +Ookii.CommandLine doesn't assign the property if the argument is not specifies), but that default +value would not be included in the usage help, whereas the default value of `Arg1` will be. -With source generation, both `Arg1` and `Arg2` will have the default value of "foo" shown in the -usage help, making the two forms identical. Additionally, `Arg2` could be marked non-nullable -because it was initialized to a non-null value, something which isn't possible for `Arg1` without -initializing the property to a value that will not be used. +With the `GeneratedParserAttribute`, both `Arg1` and `Arg2` will have the default value of "foo" +shown in the usage help, making the two forms identical. Additionally, `Arg2` could be marked +non-nullable because it was initialized to a non-null value, something which isn't possible for +`Arg1` without initializing the property to a value that will not be used. If both a property initializer and the `DefaultValue` property are both used, the `DefaultValue` property takes precedence. -Note that this only works if the property initializer is a literal. If a different kind of value is -used in the property initialized, such as a reference to a constant or a function call, the value -will not be shown in the usage help. +This only works if the property initializer is a literal, enumeration value, reference to a constant, +or a null-forgiving expression with any of those expression types. + +For example, `5`, `"value"`, `DayOfWeek.Tuesday`, `int.MaxValue` and `default!` are all supported +expressions for property initializers. + +If a different kind of expression is used in the property initializer, such as a function call or +`new` expression, the value will not be shown in the usage help. ## Generating a command manager -Just like the `CommandLineParser` class, the `CommandManager` class normally uses reflection to -locate all command classes in the assembly or assemblies you specify. Instead, you can create a -class with the `GeneratedCommandManagerAttribute` which can perform this same job at compile time. +You can apply the `GeneratedParserAttribute` to a command, and generate the parser for that command +at compile time. This will work with the `CommandManager` class without further changes to your +code. -To create a generated command manager, define a partial class with the -`GeneratedCommandManagerAttribute`: +The `GeneratedParserAttribute` works the same for command classes as it does for any other arguments +class, with one exception: the static `Parse()` methods are not generated by default for command +classes. You must explicitly set the `GeneratedParserAttribute.GenerateParseMethods` to `true` if +you want them to be generated. + +However, the `CommandManager` class still uses reflection to determine what commands are available +in the assembly or assemblies you specify. To determine the available commands at compile time, you +must define a partial class with the `GeneratedCommandManagerAttribute`: ```csharp [GeneratedCommandManager] -partial class MyCommandManager +partial class GeneratedManager { } ``` The source generator will find all command classes in your project, and generate C# code to provide -those arguments to the `CommandManager` without needing reflection. +those command to the generated command manager without needing reflection. If you need to load commands from a different assembly, or multiple assemblies, you can use the `GeneratedCommandManagerAttribute.AssemblyNames` property. This property can use either just the @@ -188,13 +239,14 @@ token. ```csharp [GeneratedCommandManager(AssemblyNames = new[] { "MyCommandAssembly" })] -partial class MyCommandManager +partial class GeneratedManager { } ``` -If you wish to use commands from an assembly that is dynamically loaded during runtime, you must -continue using reflection. +Any assemblies specified in this list must be directly referenced by your application. If you wish +to use commands from an assembly that is dynamically loaded during runtime, you must continue to use +reflection. ### Using a generated command manager @@ -202,10 +254,11 @@ The source generator will add `CommandManager` as a base class to your class, an following constructor to the class: ```csharp -public MyCommandManager(CommandOptions? options = null) +public GeneratedManager(CommandOptions? options = null) ``` -Instead of instantiation the `CommandManager` class, you use your generated class instead. +This means a class with the `GeneratedCommandManagerAttribute` can be used as a drop-in replacement +of the regular `CommandManager` class. If you had the following code before using source generation: @@ -217,38 +270,9 @@ return manager.RunCommand() ?? 1; You would replace it with the following: ```csharp -var manager = new MyCommandManager(); +var manager = new GeneratedManager(); return manager.RunCommand() ?? 1; ``` -### Commands with generated parsers - -You can apply the `GeneratedParserAttribute` to a command class, and a generated command manager -will use the generated parser for that command. - -```csharp -[Command] -[GeneratedParser] -partial class MyCommand : ICommand -{ - [CommandLineArgument] - public string? SomeArgument { get; set; } - - public int Run() - { - /* ... */ - } -} -``` - -Note that if you create a normal `CommandManager` instance which uses reflection, it will always use -reflection to create a parser for its commands, even if the command has the -`GeneratedParserAttribute`. - -The `GeneratedParserAttribute` works the same for command classes as it does for any other arguments -class, with one exception: the static `Parse()` methods are not generated by default for command -classes. You must explicitly set the `GeneratedParserAttribute.GenerateParseMethods` to `true` if -you want them to be generated. - Next, we will take a look at several [utility classes](Utilities.md) provided, and used, by Ookii.CommandLine. From fe2cbe98209dd73cd6b35357e8e4bd32ab2e43a8 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 13 Jun 2023 18:17:49 -0700 Subject: [PATCH 136/234] Readme and partial tutorial update. --- README.md | 75 +++++++----- docs/Tutorial.md | 309 ++++++++++++++++++++++------------------------- 2 files changed, 186 insertions(+), 198 deletions(-) diff --git a/README.md b/README.md index cfc66b8e..2bce02e2 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, are 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 required int Optional { get; set; } = 42; [CommandLineArgument] [Description("An argument that can only be supplied by name.")] @@ -62,12 +65,17 @@ 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(); ``` +The `Parse()` method is added to the class through [source generation](docs/SourceGeneration.md). + This code 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,26 @@ 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) -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 `ISpanParsable` and `IParsable`. ## 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 +162,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/). @@ -169,11 +180,11 @@ Nowadays, System.CommandLine offers an official Microsoft solution for command l 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. +line arguments, using properties and attributes, which I personally prefer to the fluent API used +by System.CommandLine, as it reduces the amount of code you typically need to write. -Additionally, Ookii.CommandLine supports a more PowerShell-like syntax, as well as the POSIX-like -syntax that System.CommandLine uses. +Additionally, Ookii.CommandLine is highly configurable, and supports a more PowerShell-like syntax, +as well as the POSIX-like syntax that System.CommandLine uses. In the end, it comes down to personal preference. You should use whichever one suits your needs and coding style best. diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 3d8a7daa..9d930326 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -2,7 +2,7 @@ This tutorial will show you the basics of how to use Ookii.CommandLine. It will show you how to create an application that parses the command line and shows usage help, how to customize some of -the options—including the new long/short mode—and how to use subcommands. +the options—including the POSIX-like long/short mode—and how to use subcommands. Refer to the [documentation](README.md) for more detailed information. @@ -12,7 +12,7 @@ Create a directory called "tutorial" for the project, and run the following comm directory: ```text -dotnet new console --framework net6.0 +dotnet new console --framework net7.0 ``` Next, we will add a reference to Ookii.CommandLine's NuGet package: @@ -26,70 +26,77 @@ dotnet add package Ookii.CommandLine Add a file to your project called Arguments.cs, and insert the following code: ```csharp +using System.ComponentModel; using Ookii.CommandLine; namespace Tutorial; -class Arguments +[GeneratedParser] +[Description("Reads a file and displays the contents on the command line.")] +partial class Arguments { - [CommandLineArgument(Position = 0, IsRequired = true)] - public string? Path { get; set; } + [CommandLineArgument(IsPositional = true)] + [Description("The path of the file to read.")] + public required string Path { get; set; } } ``` +If you are targeting a .Net version before .Net 7.0, the `required` keyword is not available. In +that case, use the following code instead: + +```csharp +[CommandLineArgument(IsPositional = true, IsRequired = true)] +[Description("The path of the file to read.")] +public string? Path { get; set; } +``` + In Ookii.CommandLine, you define arguments by making a class that holds them, and adding properties to that class. Every public property that has the [`CommandLineArgumentAttribute`][] defines an argument. -The code above defines a single argument called "Path", indicates it's the first positional argument, +The code above defines a single argument called "Path", indicates it's the a positional argument, and makes it required. > You can use the [`CommandLineArgumentAttribute`][] to specify a custom name for your argument. If you > don't, the property name is used. +The class above uses the `GeneratedParserAttribute`, which is not required, but is recommended +unless you are using an SDK older than .Net 6.0, or a language other than C# ([find out more](SourceGeneration.md)). + Now replace the contents of Program.cs with the following: ```csharp -using Ookii.CommandLine; +using Tutorial; -namespace Tutorial; - -static class Program +var arguments = Arguments.Parse(); +if (arguments == null) { - public static int Main() - { - var args = CommandLineParser.Parse(); - if (args == null) - { - return 1; - } - - ReadFile(args); - return 0; - } + return 1; +} - private static void ReadFile(Arguments args) - { - foreach (var line in File.ReadLines(args.Path!)) - { - Console.WriteLine(line); - } - } +foreach (var line in File.ReadLines(arguments.Path)) +{ + Console.WriteLine(line); } + +return 0; ``` This code parses the arguments we defined, returns an error code if it was unsuccessful, and writes the contents of the file specified by the path argument to the console. -The important part is the call to `CommandLineParser.Parse()`. This static method will -parse your arguments, handle and print any errors, and print usage help if required. +The important part is the call to `Arguments.Parse()`. This static method was created by the +`GeneratedParserAttribute`, and will parse your arguments, handle and print any errors, and print +usage help if required. -But wait, we didn't pass any arguments to this method? Actually, the [`Parse()`][Parse()_1] -method will call [`Environment.GetCommandLineArgs()`][] to get the arguments. There are also -overloads that take an explicit `string[]` array with the arguments, if you want to pass them -manually. +> If you cannot use the `GeneratedParserAttribute`, call `CommandLineParser.Parse()` +> instead. + +But wait, we didn't pass any arguments to this method? Actually, the method will call +[`Environment.GetCommandLineArgs()`][] to get the arguments. There are also overloads that take an +explicit `string[]` array with the arguments, if you want to pass them manually. So, let's run our application. Build the application using `dotnet build`, and then, from the -`bin/Debug/net6.0` directory, run the following: +`bin/Debug/net7.0` directory, run the following: ```text ./tutorial ../../../tutorial.csproj @@ -102,13 +109,13 @@ Which will give print the contents of the tutorial.csproj file: Exe - net6.0 + net7.0 enable enable - + @@ -126,8 +133,13 @@ This gives the following output: ```text The required argument 'Path' was not supplied. +Reads a file and displays the contents on the command line. + Usage: tutorial [-Path] [-Help] [-Version] + -Path + The path of the file to read. + -Help [] (-?, -h) Displays this help message. @@ -138,13 +150,13 @@ Usage: tutorial [-Path] [-Help] [-Version] > The actual usage help uses color if your console supports it. See [here](images/color.png) for > an example. -As you can see, the [`Parse()`][Parse()_1] method lets us know what's wrong (we didn't supply -the required argument), and shows the usage help. +As you can see, the generated `Parse()` method lets us know what's wrong (we didn't supply the +required argument), and shows the usage help. -The usage syntax (the line starting with "Usage:") includes the `-Path` argument we defined. -However, the list of argument descriptions below it does not. That's because our argument doesn't -have a description, and only arguments with descriptions are shown in that list by default. We'll -add some descriptions [below](#expanding-the-usage-help). +This usage help includes the description we applied to the class (this is the application +description), and the `-Path` argument using the [`DescriptionAttribute`][]. This is how you can +provide detailed information about your arguments to your users. It's strongly recommended to +always add a description to your arguments. You can also see that there are two more arguments that we didn't define: `-Help` and `-Version`. These arguments are automatically added by Ookii.CommandLine. So, what do they do? @@ -163,14 +175,14 @@ tutorial 1.0.0 By default, it shows the assembly's name and informational version. It'll also show the assembly's copyright information, if there is any (there's not in this case). You can also use the -[`ApplicationFriendlyNameAttribute`][] attribute to specify a custom name instead of the assembly name. +`AssemblyTitleAttribute` or [`ApplicationFriendlyNameAttribute`][] attribute to specify a custom +name instead of the assembly name. > If you define an argument called "Help" or "Version", the automatic arguments won't be added. > Also, you can disable the automatic arguments using the [`ParseOptionsAttribute`][] attribute. -Note that in the usage syntax, your positional "Path" argument still has its name shown as `-Path`. -That's because every argument, even positional ones, can still be supplied by name. So if you run -this: +Note that the positional "Path" argument still has its name shown as `-Path`. That's because every +argument, even positional ones, can still be supplied by name. So if you run this: ```text ./tutorial -path ../../../tutorial.csproj @@ -178,8 +190,8 @@ this: The output is the same as above. -> Argument names are case insensitive by default, so even though I used `-path` instead of `-Path` -> above, it still worked. +> Argument names are case insensitive by default, so `-path` will work instead of `-Path`, as does +> `-PATH` or any other capitalization. ## Arguments with other types @@ -200,18 +212,21 @@ And then add the following properties to the `Arguments` class: ```csharp [CommandLineArgument] +[Description("The maximum number of lines to output.")] +[ValueDescription("Number")] [ValidateRange(1, null)] -[Alias("Max")] +[Alias("Lines")] public int? MaxLines { get; set; } [CommandLineArgument] +[Description("Use black text on a white background.")] public bool Inverted { get; set; } ``` This defines two new arguments. The first, `-MaxLines`, uses `int?` as its type, so it will only accept integer numbers, and be null if not supplied. This argument is not positional (you must use the name), and it's optional. We've also added a validator to ensure the value is positive, and -since `-MaxLines` might be a bit verbose, we've given it an alias `-Max`, which can be used as an +since `-MaxLines` might be a bit verbose, we've given it an alias `-Lines`, which can be used as an alternative name to supply the argument. > An argument can have any number of aliases; just repeat the [`AliasAttribute`][] attribute. @@ -219,39 +234,46 @@ alternative name to supply the argument. The second argument, `-Inverted`, is a boolean, which means it's a switch argument. Switch arguments don't need values, you either supply them or you don't. -Now, let's update `ReadFile` to use the new arguments: +Now, let's update Program.cs to use the new arguments: ```csharp -private static void ReadFile(Arguments args) +using Tutorial; + +var arguments = Arguments.Parse(); +if (arguments == null) { - if (args.Inverted) - { - Console.BackgroundColor = ConsoleColor.White; - Console.ForegroundColor = ConsoleColor.Black; - } + return 1; +} - var lines = File.ReadLines(args.Path!); - if (args.MaxLines is int maxLines) - { - lines = lines.Take(maxLines); - } +if (arguments.Inverted) +{ + Console.BackgroundColor = ConsoleColor.White; + Console.ForegroundColor = ConsoleColor.Black; +} - foreach (var line in lines) - { - Console.WriteLine(line); - } +var lines = File.ReadLines(arguments.Path); +if (arguments.MaxLines is int maxLines) +{ + lines = lines.Take(maxLines); +} - if (args.Inverted) - { - Console.ResetColor(); - } +foreach (var line in lines) +{ + Console.WriteLine(line); } + +if (arguments.Inverted) +{ + Console.ResetColor(); +} + +return 0; ``` Now we can run the application like this: ```text -./tutorial ../../../tutorial.csproj -max 5 -inverted +./tutorial ../../../tutorial.csproj -lines 5 -inverted ``` And it'll only show the first five lines of the file, using black-on-white text. @@ -259,97 +281,21 @@ And it'll only show the first five lines of the file, using black-on-white text. If you supply a value that's not a valid integer for `-MaxLines`, or a value that's less than 1, you'll once again get an error message and the usage help. -Above, we used a nullable value type ([`Nullable`][], or `int?`) so we could tell whether the -argument was supplied. Instead, we could also set a default value. This can be done in two ways: the -first is using the [`DefaultValue`][DefaultValue_1] property: +What do you think will happen if we run this command? -```csharp -[CommandLineArgument(DefaultValue = 10)] -[ValidateRange(1, null)] -[Alias("Max")] -public int MaxLines { get; set; } -``` - -> If your argument's type doesn't have literals, you can also use a string to specify the default -> value, and the value will be converted when used. For example, `[CommandLineArgument(DefaultValue = "10")]` -> is equivalent to the above. - -Alternatively, you can just initialize the property, since Ookii.CommandLine won't set the property -if the argument is not supplied and the default value is null: - -```csharp -[CommandLineArgument] -public int MaxLines { get; set; } = 10; -``` - -The advantage of the former approach is that the default value will be included in the usage help. -The latter allows you to use non-constant values, and can sometimes be required if the type of an -argument is a non-nullable reference type (you can also use both, in which case the [`DefaultValue`][DefaultValue_1] -property will overwrite the initial value). - -While we're talking about non-nullable reference types, consider the following alternative for the -`-Path` argument: - -```csharp -[CommandLineArgument(Position = 0, IsRequired = true)] -public string Path { get; set; } = string.Empty; -``` - -An automatic property with a non-nullable type must be initialized with a non-null value, or the -code won't compile. Even though we know the property will be set by the [`CommandLineParser`][], -because the argument is required, this is still required because the C# compiler can't know that -(and the compiler is right in case you create an instance manually without using the -[`CommandLineParser`][]). So we must initialize the property, even if that value won't be used. - -The advantage of doing this would be that we can remove the `!` from the value's usage in -`ReadFile`, at the cost of an unnecessary initialization. As a bonus, for .Net 6.0 and later only, -the [`CommandLineParser`][] will make sure that arguments with non-nullable types can't be set to -null, even if the [`TypeConverter`][] for the property's type returns null (it will treat that as an -error). - -## Expanding the usage help - -We saw before that our custom arguments were included in the usage syntax, but didn't have any -descriptions. Typically, you'll want to add descriptions to your arguments, so your users can tell -what they do. This is done using the [`System.ComponentModel.DescriptionAttribute`][] attribute. - -Let's add some for our arguments: - -```csharp -using Ookii.CommandLine; -using Ookii.CommandLine.Validation; -using System.ComponentModel; - -namespace Tutorial; - -[Description("Reads a file and displays the contents on the command line.")] -class Arguments -{ - [CommandLineArgument(Position = 0, IsRequired = true)] - [Description("The path of the file to read.")] - public string? Path { get; set; } - - [CommandLineArgument(ValueDescription = "Number")] - [Description("The maximum number of lines to output.")] - [ValidateRange(1, null)] - [Alias("Max")] - public int? MaxLines { get; set; } - - [CommandLineArgument] - [Description("Use black text on a white background.")] - public bool Inverted { get; set; } -} +```text +./tutorial ../../../tutorial.csproj -m 5 -i ``` -I've also added a description to the class itself. That description is shown before the usage syntax -as part of the usage help. Use it to provide a description for your application as a whole. +If you tried it, you can see that it worked. By default, Ookii.CommandLine will treat any unique +prefix of a command line argument's name or aliases as an alias for that command. So, `-m` is +automatically an alias for `-MaxLines`. As is `-ma`, and `-max`, etc. And `-l` is as well, as it's +a prefix of the alias `-Lines`. -The `MaxLines` property now also sets its *value description*. The value description is a short, -typically one-word description of the type of values the argument accepts, which is shown in angle -brackets in the usage help. It defaults to the type name, but "Int32" might not be very meaningful -to people who aren't programmers, so we've changed it to "Number" instead. +> This only works if the prefix matches exactly one argument. And if you don't like this behavior, +> is can be disabled using the `ParseOptionsAttribute.AutoPrefixAliases` property. -Now, let's run the application using `./tutorial -help`: +Let's take a look at the usage help for our updated application, by running `./tutorial -help`: ```text Reads a file and displays the contents on the command line. @@ -365,23 +311,56 @@ Usage: tutorial [-Path] [-Help] [-Inverted] [-MaxLines ] [-Vers -Inverted [] Use black text on a white background. - -MaxLines (-Max) + -MaxLines (-Lines) The maximum number of lines to output. Must be at least 1. -Version [] Displays version information. ``` -Now our usage help looks a lot better! All the arguments are present in the description list. Also -note how the [`ValidateRangeAttribute`][] validator we used automatically added its condition to the -description of `-MaxLines` (this can be disabled either globally or on a per-validator basis if you -want). Things like the default value, if an argument has one, are added in a similar fashion. +There's a few interesting things here. The `MaxLines` property has the `ValueDescriptionAttribute` +applied, and we can see that the value, "Number", is used inside the angle brackets after +`-MaxLines`. This is the *value description*, which is a short, typically one-word description of +the type of values the argument accepts. It defaults to the type name, but "Int32" might not be very +meaningful to people who aren't programmers, so we've changed it to "Number" instead. + +You can also see that the [`ValidateRangeAttribute`][] doesn't just validate its condition, it also +adds that condition to the description of the argument (this can be disabled either globally or on +a per-validator basis if you want). So you don't have to worry about keeping the description and +the actual requirements in sync. The `-MaxLines` argument also has its alias listed, just like the `-Help` argument. > Don't like the way the usage help looks? It can be fully customized! Check out the > [custom usage sample](../src/Samples/CustomUsage) for an example of that. +## Default values + +Above, we used a nullable value type ([`Nullable`][], or `int?`) so we could tell whether the +argument was supplied. Instead, we could also set a default value. This can easily be done by +initializing the property with that value: + +```csharp +[CommandLineArgument] +[ValidateRange(1, null)] +[Alias("Lines")] +public int MaxLines { get; set; } = 10; +``` + +> Instead of initializing the property, you can also use the +> [`CommandLineArgumentAttribute.DefaultValue`][] property, which can be useful if e.g. you're not using +> an automatic property (so you can't have a direct initializer like that). And, this method accepts +> not just the argument's actual type, but also any string that can be converted to it. For example, +> both `[CommandLineArgument(DefaultValue = 10)]` and `[CommandLineArgument(DefaultValue = "10")]` +> are equivalent to the above. Handy if your argument's type doesn't have literals. + +This default value would be shown in the usage help as well, similar to the validator: + +```text + -MaxLines (-Lines) + The maximum number of lines to output. Must be at least 1. Default value: 10. +``` + ## Long/short mode and other customizations Ookii.CommandLine offers many options to customize the way it parses the command line. For example, @@ -1001,6 +980,7 @@ following resources: [`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm [`CaseSensitive`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive.htm [`CommandAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_CommandAttribute.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.ShortName`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm [`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm @@ -1031,13 +1011,10 @@ following resources: [`StringComparer.InvariantCulture`]: https://learn.microsoft.com/dotnet/api/system.stringcomparer.invariantculture [`StringComparer.Ordinal`]: https://learn.microsoft.com/dotnet/api/system.stringcomparer.ordinal [`StringComparer.OrdinalIgnoreCase`]: https://learn.microsoft.com/dotnet/api/system.stringcomparer.ordinalignorecase -[`System.ComponentModel.DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute [`Take()`]: https://learn.microsoft.com/dotnet/api/system.linq.enumerable.take -[`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter [`Uri`]: https://learn.microsoft.com/dotnet/api/system.uri [`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm [ArgumentNameComparer_1]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparer.htm -[DefaultValue_1]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm [Mode_2]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_Mode.htm [Parse()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm [Run()_0]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm From 3f13e160dda117c6d610c9fcaddacf74d72632e7 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 14 Jun 2023 11:58:39 -0700 Subject: [PATCH 137/234] Change ShowUsageOnError default. --- src/Ookii.CommandLine.Tests/CommandLineParserTest.cs | 1 + src/Ookii.CommandLine.Tests/CommandOptionsTest.cs | 2 +- src/Ookii.CommandLine.Tests/ParseOptionsTest.cs | 2 +- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 1 + src/Ookii.CommandLine/ParseOptions.cs | 2 +- src/Ookii.CommandLine/UsageHelpRequest.cs | 8 ++++---- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 65c97e67..76e41b19 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -471,6 +471,7 @@ public void TestStaticParse(ProviderKind kind) { ArgumentNamePrefixes = new[] { "/", "-" }, Error = error, + ShowUsageOnError = UsageHelpRequest.Full, UsageWriter = new UsageWriter(lineWriter) { ExecutableName = _executableName, diff --git a/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs b/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs index ecd6914e..06dc8661 100644 --- a/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs @@ -37,7 +37,7 @@ public void TestConstructor() Assert.IsNull(options.LongArgumentNamePrefix); Assert.IsNull(options.Mode); Assert.IsNull(options.NameValueSeparators); - Assert.AreEqual(UsageHelpRequest.Full, options.ShowUsageOnError); + Assert.AreEqual(UsageHelpRequest.SyntaxOnly, options.ShowUsageOnError); Assert.IsNotNull(options.StringProvider); Assert.IsNotNull(options.UsageWriter); Assert.IsNull(options.UseErrorColor); diff --git a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs index a1c8955e..ca7593a4 100644 --- a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs +++ b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs @@ -36,7 +36,7 @@ public void TestConstructor() Assert.IsNull(options.LongArgumentNamePrefix); Assert.IsNull(options.Mode); Assert.IsNull(options.NameValueSeparators); - Assert.AreEqual(UsageHelpRequest.Full, options.ShowUsageOnError); + Assert.AreEqual(UsageHelpRequest.SyntaxOnly, options.ShowUsageOnError); Assert.IsNotNull(options.StringProvider); Assert.IsNotNull(options.UsageWriter); Assert.IsNull(options.UseErrorColor); diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 4df78574..2b1c2ba1 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -418,6 +418,7 @@ public void TestParentCommandUsage(ProviderKind kind) var options = new CommandOptions() { Error = writer, + ShowUsageOnError = UsageHelpRequest.Full, UsageWriter = new UsageWriter(writer) { ExecutableName = _executableName, diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 0b8fabef..feeb2ff8 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -638,7 +638,7 @@ public LocalizedStringProvider StringProvider /// /// /// One of the values of the enumeration. The default value - /// is . + /// is . /// /// /// diff --git a/src/Ookii.CommandLine/UsageHelpRequest.cs b/src/Ookii.CommandLine/UsageHelpRequest.cs index 03a722cb..e066e48d 100644 --- a/src/Ookii.CommandLine/UsageHelpRequest.cs +++ b/src/Ookii.CommandLine/UsageHelpRequest.cs @@ -6,16 +6,16 @@ /// 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, /// + /// Full usage help is shown, including the argument descriptions. + /// + Full, + /// /// No usage help is shown. Instead, the /// message is shown. /// From 71eef376e66c0deeb8183a899cc824b4f59db703 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 14 Jun 2023 12:48:32 -0700 Subject: [PATCH 138/234] Automatically show command help instruction when able. --- .../SubCommandTest.Usage.cs | 16 + src/Ookii.CommandLine.Tests/SubCommandTest.cs | 22 + src/Ookii.CommandLine/UsageWriter.cs | 3966 +++++++++-------- 3 files changed, 2053 insertions(+), 1951 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs index 597221ea..f527172c 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.Usage.cs @@ -70,6 +70,22 @@ 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] diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 2b1c2ba1..c759dd09 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -209,6 +209,28 @@ public void TestWriteUsageInstruction(ProviderKind kind) Assert.AreEqual(_expectedUsageInstruction, writer.BaseWriter.ToString()); } + [TestMethod] + [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] + public void TestWriteUsageAutoInstruction(ProviderKind kind) + { + using var writer = LineWrappingTextWriter.ForStringWriter(0); + var options = new CommandOptions() + { + Error = writer, + // Filter out commands that prevent the instruction being shown. + CommandFilter = c => !c.UseCustomArgumentParsing, + UsageWriter = new UsageWriter(writer) + { + ExecutableName = _executableName, + IncludeCommandHelpInstruction = true, + } + }; + + var manager = CreateManager(kind, options); + manager.WriteUsage(); + Assert.AreEqual(_expectedUsageAutoInstruction, writer.BaseWriter.ToString()); + } + [TestMethod] [DynamicData(nameof(ProviderKinds), DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))] public void TestWriteUsageApplicationDescription(ProviderKind kind) diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index eb82a6e0..e768e487 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -9,2166 +9,2230 @@ 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 for the + /// mode. + /// + /// + /// 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 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 + { + 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 + /// . + /// + /// + /// 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 first item 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; + + /// + /// 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; and + /// 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. + /// + /// + 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 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) + { + _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()) + { + if (argument.IsHidden) + { + continue; + } - // Don't apply indentation to console output if the line width is less than this. - private const int MinimumLineWidthForIndent = 30; + Write(" "); + if (UseAbbreviatedSyntax && argument.Position == null) + { + WriteAbbreviatedRemainingArguments(); + break; + } - private const char OptionalStart = '['; - private const char OptionalEnd = ']'; + if (argument.IsRequired) + { + WriteArgumentSyntax(argument); + } + else + { + WriteOptionalArgumentSyntax(argument); + } + } - private LineWrappingTextWriter? _writer; - private bool? _useColor; - private CommandLineParser? _parser; - private CommandManager? _commandManager; - private string? _executableName; - private string? _defaultExecutableName; - private bool _includeExecutableExtension; + WriteUsageSyntaxSuffix(); + WriteLine(); // End syntax line + WriteLine(); // Blank line + } - /// - /// 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) + /// + /// 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. + /// + /// + protected virtual IEnumerable GetArgumentsInUsageOrder() => Parser.Arguments; + + /// + /// 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() + { + 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); + } - /// - /// 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 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.IsMultiValue) + { + 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. + } + } + + /// + /// 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 first item 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. - /// - /// - /// If the property is not , - /// and the specified type has a , that description is - /// used instead. - /// - /// - 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 . + /// + /// + 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.DefaultValue != null) { - Writer.ResetIndent(); - SetIndent(SyntaxIndent); + WriteDefaultValue(argument.DefaultValue); + } - WriteUsageSyntaxPrefix(); - foreach (CommandLineArgument argument in GetArgumentsInUsageOrder()) - { - 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 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) + { + Write(OptionalStart); + WriteValueDescriptionForDescription(valueDescription); + Write(OptionalEnd); + } - WriteUsageSyntaxSuffix(); - 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; } - /// - /// 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. - /// - /// - protected virtual IEnumerable GetArgumentsInUsageOrder() => Parser.Arguments; + var count = WriteAliasHelper(shortPrefix, shortAliases, 0); + count = WriteAliasHelper(prefix, aliases, count); - /// - /// 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() + 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); + } + + /// + /// 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); } } + } - /// - /// 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() + /// + /// 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 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) { - if (OperationInProgress == Operation.CommandListUsage) + var name = ExecutableName; + if (CommandName != null) { - WriteLine(Resources.DefaultCommandUsageSuffix); + name += " " + CommandName; } - } - /// - /// 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); + WriteLine(Resources.MoreInfoOnErrorFormat, name, arg.ArgumentNameWithPrefix); } + } - /// - /// 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) + /// + /// 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 { - 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); - } + 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); - - /// - /// 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); - } - } + SetIndent(SyntaxIndent); + WriteCommandListUsageSyntax(); + Writer.ResetIndent(); + Writer.Indent = 0; + WriteAvailableCommandsHeader(); - if (hasHelp) - { - WriteLine(); // Blank line. - } - } + WriteCommandDescriptions(); - /// - /// 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 (CheckShowCommandHelpInstruction()) { - if (ArgumentDescriptionListFilter == DescriptionListFilterMode.None) - { - return; - } + var prefix = CommandManager.Options.Mode == ParsingMode.LongShort + ? (CommandManager.Options.LongArgumentNamePrefixOrDefault) + : (CommandManager.Options.ArgumentNamePrefixes?.FirstOrDefault() ?? CommandLineParser.GetDefaultArgumentNamePrefixes()[0]); - 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; - } - } + var transform = CommandManager.Options.ArgumentNameTransformOrDefault; + var argumentName = transform.Apply(CommandManager.Options.StringProvider.AutomaticHelpName()); - var arguments = GetArgumentsInDescriptionOrder(); - bool first = true; - foreach (var argument in arguments) + Writer.Indent = 0; + var name = ExecutableName; + if (CommandName != null) { - if (first) - { - WriteArgumentDescriptionListHeader(); - first = false; - } - - WriteArgumentDescription(argument); + name += " " + CommandName; } - } - /// - /// 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. + WriteCommandHelpInstruction(name, prefix, argumentName); } + } - /// - /// 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 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 (BlankLineAfterDescription) - { - WriteLine(); - } - } + /// + /// 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(); + } - /// - /// 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 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()) { - 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 + if (command.IsHidden) { - WriteArgumentNameForDescription(argument.ArgumentName, prefix); + continue; } - Write(' '); - if (argument.IsSwitch) - { - WriteSwitchValueDescription(argument.ValueDescription); - } - else - { - WriteValueDescriptionForDescription(argument.ValueDescription); - } + WriteCommandDescription(command); + } + } - if (IncludeAliasInDescription) - { - WriteAliases(argument.Aliases, argument.ShortAliases, prefix, shortPrefix); - } + /// + /// 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); - ResetColor(); + if (BlankLineAfterCommandDescription) + { 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) + /// + /// 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) { - bool hasDescription = !string.IsNullOrEmpty(argument.Description); - if (hasDescription) - { - WriteArgumentDescription(argument.Description); - } - - if (IncludeValidatorsInDescription) - { - WriteArgumentValidators(argument); - } + WriteCommandAliases(command.Aliases); + } - if (IncludeDefaultValueInDescription && argument.DefaultValue != null) - { - WriteDefaultValue(argument.DefaultValue); - } + ResetColor(); + WriteLine(); + } + /// + /// 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 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) + /// + /// 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) + { + foreach (var alias in aliases) { - Write(prefix); - Write(argumentName); + 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 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 , 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 , 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 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) + { + if (UseColor) { - foreach (var validator in argument.Validators) - { - var help = validator.GetUsageHelp(argument); - if (!string.IsNullOrEmpty(help)) - { - Write(' '); - Write(help); - } - } + Write(color); } + } - /// - /// 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 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); - /// - /// 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) - { - name += " " + 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 virtual IEnumerable GetArgumentsInDescriptionOrder() + 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.ArgumentNameComparison.GetComparer(); - - 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); - } - } - - SetIndent(SyntaxIndent); - WriteCommandListUsageSyntax(); - Writer.ResetIndent(); - Writer.Indent = 0; - WriteAvailableCommandsHeader(); + WriteOptionalArgumentSyntax(argument); + } - WriteCommandDescriptions(); + writer.Flush(); + return writer.BaseWriter.ToString()!; + } - if (IncludeCommandHelpInstruction) - { - var prefix = CommandManager.Options.Mode == ParsingMode.LongShort - ? (CommandManager.Options.LongArgumentNamePrefixOrDefault) - : (CommandManager.Options.ArgumentNamePrefixes?.FirstOrDefault() ?? CommandLineParser.GetDefaultArgumentNamePrefixes()[0]); + private void WriteLine(string? value) + { + Write(value); + WriteLine(); + } - var transform = CommandManager.Options.ArgumentNameTransformOrDefault; - var argumentName = transform.Apply(CommandManager.Options.StringProvider.AutomaticHelpName()); + private void Write(string format, object? arg0) => Write(string.Format(Writer.FormatProvider, format, arg0)); - Writer.Indent = 0; - var name = ExecutableName; - if (CommandName != null) - { - name += " " + CommandName; - } + private void WriteLine(string format, object? arg0, object? arg1) + => WriteLine(string.Format(Writer.FormatProvider, format, arg0, arg1)); - WriteCommandHelpInstruction(name, 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 . - /// - /// - /// This method is called by the base implementation of the - /// method. - /// - /// - protected virtual void WriteCommandListUsageSyntax() + private VirtualTerminalSupport? EnableColor() + { + if (_useColor == null && _writer == null) { - WriteUsageSyntaxPrefix(); - WriteUsageSyntaxSuffix(); - 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 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 . - /// 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 name, string argumentNamePrefix, string argumentName) - { - WriteLine(Resources.CommandHelpInstructionFormat, name, 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; } } From 85d7042a00a6a31ffa337247a0518923bb06a876 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 14 Jun 2023 12:57:39 -0700 Subject: [PATCH 139/234] Simplified span validation. --- src/Ookii.CommandLine/CommandLineArgument.cs | 3 +- .../Validation/ArgumentValidationAttribute.cs | 34 +++++++++---------- .../Validation/ValidateNotEmptyAttribute.cs | 10 +----- .../ValidateNotWhiteSpaceAttribute.cs | 10 +----- .../Validation/ValidatePatternAttribute.cs | 5 +-- .../ValidateStringLengthAttribute.cs | 3 +- 6 files changed, 22 insertions(+), 43 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index bb7d3e3c..c3a750d2 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1619,9 +1619,8 @@ private void PreValidate(ref string? stringValue, ReadOnlySpan spanValue) { if (stringValue == null) { - if (validator.CanValidateSpan) + if (validator.ValidateSpan(this, spanValue)) { - validator.ValidateSpan(this, spanValue); continue; } else diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs index d7e8a20f..f64eeeb2 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs @@ -47,16 +47,6 @@ public abstract class ArgumentValidationAttribute : Attribute /// public virtual CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.ValidationFailed; - /// - /// Gets a value that indicates whether this instance can validate a - /// when using . - /// - /// - /// if the validator implements , otherwise, - /// . The default value is . - /// - public bool CanValidateSpan { get; protected set; } - /// /// Validates the argument value, and throws an exception if validation failed. /// @@ -90,10 +80,14 @@ public void Validate(CommandLineArgument argument, object? value) /// 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. + /// /// /// /// The class will only call this method if the - /// property is , and the /// property is . /// /// @@ -101,17 +95,20 @@ public void Validate(CommandLineArgument argument, object? value) /// The parameter is not a valid value. The /// property will be the value of the property. /// - public void ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) + public bool ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) { if (argument == null) { throw new ArgumentNullException(nameof(argument)); } - if (!IsSpanValid(argument, value)) + var result = IsSpanValid(argument, value); + if (result == false) { throw new CommandLineArgumentException(GetErrorMessage(argument, value.ToString()), argument.ArgumentName, ErrorCategory); } + + return result != null; } @@ -160,12 +157,13 @@ public void ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) /// The raw string argument value provided by the user on the command line. /// /// - /// if the value is valid; otherwise, . + /// 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 , and the /// property is . /// /// @@ -173,9 +171,11 @@ public void ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) /// 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) - => throw new NotImplementedException(Properties.Resources.IsSpanValidNotImplemented); + public virtual bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) => null; /// /// Gets the error message to display if validation failed. diff --git a/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs index e10716cc..a75c0769 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs @@ -19,14 +19,6 @@ namespace Ookii.CommandLine.Validation /// public class ValidateNotEmptyAttribute : ArgumentValidationWithHelpAttribute { - /// - /// Initializes a new instance of the class. - /// - public ValidateNotEmptyAttribute() - { - CanValidateSpan = true; - } - /// /// Gets a value that indicates when validation will run. /// @@ -51,7 +43,7 @@ public override bool IsValid(CommandLineArgument argument, object? value) } /// - public override bool IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) => !value.IsEmpty; /// diff --git a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs index 62bbcf21..b3324c91 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs @@ -21,14 +21,6 @@ namespace Ookii.CommandLine.Validation /// public class ValidateNotWhiteSpaceAttribute : ArgumentValidationWithHelpAttribute { - /// - /// Initializes a new instance of the class. - /// - public ValidateNotWhiteSpaceAttribute() - { - CanValidateSpan = true; - } - /// /// Gets a value that indicates when validation will run. /// @@ -53,7 +45,7 @@ public override bool IsValid(CommandLineArgument argument, object? value) } /// - public override bool IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) => !value.IsWhiteSpace(); /// diff --git a/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs b/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs index 1f23b8d0..74c4afb9 100644 --- a/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs @@ -39,9 +39,6 @@ public ValidatePatternAttribute(string pattern, RegexOptions options = RegexOpti { _pattern = pattern; _options = options; -#if NET7_0_OR_GREATER - CanValidateSpan = true; -#endif } /// @@ -112,7 +109,7 @@ public override bool IsValid(CommandLineArgument argument, object? value) /// /// if the value is valid; otherwise, . /// - public override bool IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) => Pattern.IsMatch(value); #endif diff --git a/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs index 49ee9a4f..f5587d6b 100644 --- a/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs @@ -26,7 +26,6 @@ public ValidateStringLengthAttribute(int minimum, int maximum = int.MaxValue) { _minimum = minimum; _maximum = maximum; - CanValidateSpan = true; } /// @@ -70,7 +69,7 @@ public override bool IsValid(CommandLineArgument argument, object? value) } /// - public override bool IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) { var length = value.Length; return length >= _minimum && length <= _maximum; From a92f8ce167b7ca6b95b378d020e69390bd5d2df9 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 14 Jun 2023 13:25:49 -0700 Subject: [PATCH 140/234] Use ImmutableArray in CommandLineParser. --- .../CommandLineParserTest.cs | 7 +- src/Ookii.CommandLine/CommandLineParser.cs | 64 ++++++++++--------- .../Ookii.CommandLine.csproj | 3 +- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 76e41b19..81a4d757 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -43,8 +43,7 @@ public void ConstructorEmptyArgumentsTest(ProviderKind kind) Assert.AreEqual(argumentsType, target.ArgumentsType); Assert.AreEqual("Ookii.CommandLine Unit Tests", target.ApplicationFriendlyName); Assert.AreEqual(string.Empty, target.Description); - Assert.AreEqual(2, target.Arguments.Count); - using var args = target.Arguments.GetEnumerator(); + Assert.AreEqual(2, target.Arguments.Length); VerifyArguments(target.Arguments, new[] { new ExpectedArgument("Help", typeof(bool), ArgumentKind.Method) { MemberName = "AutomaticHelp", Description = "Displays this help message.", IsSwitch = true, Aliases = new[] { "?", "h" } }, @@ -67,7 +66,7 @@ public void ConstructorTest(ProviderKind kind) Assert.AreEqual(argumentsType, target.ArgumentsType); Assert.AreEqual("Friendly name", target.ApplicationFriendlyName); Assert.AreEqual("Test arguments description.", target.Description); - Assert.AreEqual(18, target.Arguments.Count); + Assert.AreEqual(18, target.Arguments.Length); VerifyArguments(target.Arguments, new[] { new ExpectedArgument("arg1", typeof(string)) { MemberName = "Arg1", Position = 0, IsRequired = true, Description = "Arg1 description." }, @@ -1211,7 +1210,7 @@ public void TestDerivedClass(ProviderKind kind) { var parser = CreateParser(kind); Assert.AreEqual("Base class attribute.", parser.Description); - Assert.AreEqual(4, parser.Arguments.Count); + Assert.AreEqual(4, parser.Arguments.Length); VerifyArguments(parser.Arguments, new[] { new ExpectedArgument("BaseArg", typeof(string), ArgumentKind.SingleValue), diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 5a394be1..182c2182 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -4,6 +4,7 @@ using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; @@ -190,7 +191,7 @@ private struct PrefixInfo #endregion private readonly ArgumentProvider _provider; - private readonly List _arguments = new(); + private readonly ImmutableArray _arguments; private readonly SortedDictionary, CommandLineArgument> _argumentsByName; private readonly SortedDictionary? _argumentsByShortName; private readonly int _positionalArgumentCount; @@ -198,13 +199,10 @@ private struct PrefixInfo private readonly ParseOptions _parseOptions; private readonly ParsingMode _mode; private readonly PrefixInfo[] _sortedPrefixes; - private readonly string[] _argumentNamePrefixes; + private readonly ImmutableArray _argumentNamePrefixes; private readonly string? _longArgumentNamePrefix; - private readonly char[] _nameValueSeparators; + private readonly ImmutableArray _nameValueSeparators; - private ReadOnlyCollection? _argumentsReadOnlyWrapper; - private ReadOnlyCollection? _argumentNamePrefixesReadOnlyWrapper; - private ReadOnlyCollection? _nameValueSeparatorsReadOnlyWrapper; private List? _requiredPropertyArguments; /// @@ -368,11 +366,20 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null _sortedPrefixes = prefixInfos.OrderByDescending(info => info.Prefix.Length).ToArray(); _argumentsByName = new(new MemoryComparer(comparison)); - _positionalArgumentCount = DetermineMemberArguments(); - DetermineAutomaticArguments(); + 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. - _arguments.Sort(new CommandLineArgumentComparer(comparison)); + builder.Sort(new CommandLineArgumentComparer(comparison)); + if (builder.Count == builder.Capacity) + { + _arguments = builder.MoveToImmutable(); + } + else + { + _arguments = builder.ToImmutable(); + } VerifyPositionalArgumentRules(); } @@ -406,8 +413,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - public ReadOnlyCollection ArgumentNamePrefixes => - _argumentNamePrefixesReadOnlyWrapper ??= new(_argumentNamePrefixes); + public ImmutableArray ArgumentNamePrefixes => _argumentNamePrefixes; /// /// Gets the prefix to use for long argument names. @@ -588,7 +594,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - public ReadOnlyCollection NameValueSeparators => _nameValueSeparatorsReadOnlyWrapper ??= new(_nameValueSeparators); + public ImmutableArray NameValueSeparators => _nameValueSeparators; /// /// Gets or sets a value that indicates whether usage help should be displayed if the @@ -670,7 +676,7 @@ public IEnumerable Validators /// 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(); + public ImmutableArray Arguments => _arguments; /// /// Gets the automatic help argument or an argument with the same name, if there is one. @@ -1227,11 +1233,11 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// - public static string[] GetDefaultArgumentNamePrefixes() + public static ImmutableArray GetDefaultArgumentNamePrefixes() { return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? new[] { "-", "/" } - : new[] { "-" }; + ? ImmutableArray.Create("-", "/") + : ImmutableArray.Create("-"); } /// @@ -1245,7 +1251,7 @@ public static string[] GetDefaultArgumentNamePrefixes() /// The return value of this method is used as the default value of the property. /// /// - public static char[] GetDefaultNameValueSeparators() => new[] { ':', '=' }; + public static ImmutableArray GetDefaultNameValueSeparators() => ImmutableArray.Create(':', '='); /// /// Raises the event. @@ -1303,7 +1309,7 @@ internal static void WriteError(ParseOptions options, string message, string col } } - private static string[] DetermineArgumentNamePrefixes(ParseOptions options) + private static ImmutableArray DetermineArgumentNamePrefixes(ParseOptions options) { if (options.ArgumentNamePrefixes == null) { @@ -1311,7 +1317,7 @@ private static string[] DetermineArgumentNamePrefixes(ParseOptions options) } else { - var result = options.ArgumentNamePrefixes.ToArray(); + var result = options.ArgumentNamePrefixes.ToImmutableArray(); if (result.Length == 0) { throw new ArgumentException(Properties.Resources.EmptyArgumentNamePrefixes, nameof(options)); @@ -1326,7 +1332,7 @@ private static string[] DetermineArgumentNamePrefixes(ParseOptions options) } } - private static char[] DetermineNameValueSeparators(ParseOptions options) + private static ImmutableArray DetermineNameValueSeparators(ParseOptions options) { if (options.NameValueSeparators == null) { @@ -1334,7 +1340,7 @@ private static char[] DetermineNameValueSeparators(ParseOptions options) } else { - var result = options.NameValueSeparators.ToArray(); + var result = options.NameValueSeparators.ToImmutableArray(); if (result.Length == 0) { throw new ArgumentException(Properties.Resources.EmptyNameValueSeparators, nameof(options)); @@ -1344,12 +1350,12 @@ private static char[] DetermineNameValueSeparators(ParseOptions options) } } - private int DetermineMemberArguments() + private int DetermineMemberArguments(ImmutableArray.Builder builder) { int additionalPositionalArgumentCount = 0; foreach (var argument in _provider.GetArguments(this)) { - AddNamedArgument(argument); + AddNamedArgument(argument, builder); if (argument.Position != null) { ++additionalPositionalArgumentCount; @@ -1359,7 +1365,7 @@ private int DetermineMemberArguments() return additionalPositionalArgumentCount; } - private void DetermineAutomaticArguments() + private void DetermineAutomaticArguments(ImmutableArray.Builder builder) { bool autoHelp = Options.AutoHelpArgumentOrDefault; if (autoHelp) @@ -1368,7 +1374,7 @@ private void DetermineAutomaticArguments() if (created) { - AddNamedArgument(argument); + AddNamedArgument(argument, builder); } HelpArgument = argument; @@ -1381,12 +1387,12 @@ private void DetermineAutomaticArguments() if (argument != null) { - AddNamedArgument(argument); + AddNamedArgument(argument, builder); } } } - private void AddNamedArgument(CommandLineArgument argument) + private void AddNamedArgument(CommandLineArgument argument, ImmutableArray.Builder builder) { if (_nameValueSeparators.Any(separator => argument.ArgumentName.Contains(separator))) { @@ -1427,7 +1433,7 @@ private void AddNamedArgument(CommandLineArgument argument) _requiredPropertyArguments.Add(argument); } - _arguments.Add(argument); + builder.Add(argument); } private void VerifyPositionalArgumentRules() @@ -1618,7 +1624,7 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri private (CancelMode, int, CommandLineArgument?) ParseNamedArgument(ReadOnlySpan args, int index, PrefixInfo prefix) { - var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitFirstOfAny(_nameValueSeparators); + var (argumentName, argumentValue) = args[index].AsMemory(prefix.Prefix.Length).SplitFirstOfAny(_nameValueSeparators.AsSpan()); CancelMode cancelParsing; CommandLineArgument? argument = null; diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index de88bacf..050c31fc 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -45,11 +45,12 @@ + - + From 7646a0e57c334ae2bbbc71ac7d28c4596b8376c9 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 14 Jun 2023 14:22:56 -0700 Subject: [PATCH 141/234] Use ImmutableArray in CommandLineArgument. --- .../CommandLineParserTest.cs | 5 ++-- src/Ookii.CommandLine/CommandLineArgument.cs | 16 +++++----- src/Ookii.CommandLine/CommandLineParser.cs | 16 ++++------ src/Samples/CustomUsage/CustomUsageWriter.cs | 30 ++++++------------- 4 files changed, 25 insertions(+), 42 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 81a4d757..b8fd7183 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -4,6 +4,7 @@ using Ookii.CommandLine.Tests.Commands; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; @@ -1356,8 +1357,8 @@ private static void VerifyArgument(CommandLineArgument argument, ExpectedArgumen Assert.IsFalse(argument.AllowMultiValueWhiteSpaceSeparator); Assert.IsNull(argument.Value); Assert.IsFalse(argument.HasValue); - CollectionAssert.AreEqual(expected.Aliases, argument.Aliases); - CollectionAssert.AreEqual(expected.ShortAliases, argument.ShortAliases); + CollectionAssert.AreEqual(expected.Aliases ?? Array.Empty(), argument.Aliases); + CollectionAssert.AreEqual(expected.ShortAliases ?? Array.Empty(), argument.ShortAliases); } private static void VerifyArguments(IEnumerable arguments, ExpectedArgument[] expected) diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index c3a750d2..c7bbcb2e 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -4,6 +4,7 @@ using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; @@ -311,8 +312,8 @@ internal struct ArgumentInfo private readonly string _argumentName; private readonly bool _hasLongName = true; private readonly char _shortName; - private readonly ReadOnlyCollection? _aliases; - private readonly ReadOnlyCollection? _shortAliases; + private readonly ImmutableArray _aliases = ImmutableArray.Empty; + private readonly ImmutableArray _shortAliases = ImmutableArray.Empty; private readonly Type _argumentType; private readonly Type _elementType; private readonly Type _elementTypeWithNullable; @@ -369,12 +370,12 @@ internal CommandLineArgument(ArgumentInfo info) if (HasLongName && info.Aliases != null) { - _aliases = new(info.Aliases.ToArray()); + _aliases = info.Aliases.ToImmutableArray(); } if (HasShortName && info.ShortAliases != null) { - _shortAliases = new(info.ShortAliases.ToArray()); + _shortAliases = info.ShortAliases.ToImmutableArray(); } _argumentType = info.ArgumentType; @@ -570,7 +571,7 @@ public string? ShortNameWithPrefix /// /// /// - public ReadOnlyCollection? Aliases => _aliases; + public ImmutableArray Aliases => _aliases; /// /// Gets the alternative short names for this command line argument. @@ -586,7 +587,7 @@ public string? ShortNameWithPrefix /// /// /// - public ReadOnlyCollection? ShortAliases => _shortAliases; + public ImmutableArray ShortAliases => _shortAliases; /// /// Gets the type of the argument's value. @@ -1349,8 +1350,7 @@ internal bool HasInformation(UsageWriter writer) return true; } - if (writer.IncludeAliasInDescription && - ((Aliases != null && Aliases.Count > 0) || (ShortAliases != null && ShortAliases.Count > 0))) + if (writer.IncludeAliasInDescription && (Aliases.Length > 0 || ShortAliases.Length > 0)) { return true; } diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 182c2182..419bed49 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1402,24 +1402,18 @@ private void AddNamedArgument(CommandLineArgument argument, ImmutableArray 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; From 52899d665f7864c4f30d25a9ea294e8bfd6544cd Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 14 Jun 2023 15:31:48 -0700 Subject: [PATCH 142/234] More tutorial updates. --- docs/Tutorial.md | 170 +++++++++++++++++++++++++++++++---------------- 1 file changed, 111 insertions(+), 59 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 9d930326..a0b57ec9 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -11,13 +11,13 @@ Refer to the [documentation](README.md) for more detailed information. Create a directory called "tutorial" for the project, and run the following command in that directory: -```text +```bash dotnet new console --framework net7.0 ``` Next, we will add a reference to Ookii.CommandLine's NuGet package: -```text +```bash dotnet add package Ookii.CommandLine ``` @@ -26,8 +26,8 @@ dotnet add package Ookii.CommandLine Add a file to your project called Arguments.cs, and insert the following code: ```csharp -using System.ComponentModel; using Ookii.CommandLine; +using System.ComponentModel; namespace Tutorial; @@ -88,8 +88,8 @@ The important part is the call to `Arguments.Parse()`. This static method was cr `GeneratedParserAttribute`, and will parse your arguments, handle and print any errors, and print usage help if required. -> If you cannot use the `GeneratedParserAttribute`, call `CommandLineParser.Parse()` -> instead. +> If you cannot use the `GeneratedParserAttribute`, call +> [`CommandLineParser.Parse()`][Parse()_1] instead. But wait, we didn't pass any arguments to this method? Actually, the method will call [`Environment.GetCommandLineArgs()`][] to get the arguments. There are also overloads that take an @@ -98,7 +98,7 @@ explicit `string[]` array with the arguments, if you want to pass them manually. So, let's run our application. Build the application using `dotnet build`, and then, from the `bin/Debug/net7.0` directory, run the following: -```text +```bash ./tutorial ../../../tutorial.csproj ``` @@ -124,7 +124,7 @@ Which will give print the contents of the tutorial.csproj file: So far, so good. But what happens if we invoke the application without arguments? After all, we made the `-Path` argument required. To try this, run the following command: -```text +```bash ./tutorial ``` @@ -133,6 +133,23 @@ This gives the following output: ```text The required argument 'Path' was not supplied. +Usage: tutorial [-Path] [-Help] [-Version] + +Run 'tutorial -Help' for more information. +``` + +As you can see, the generated `Parse()` method lets us know what's wrong (we didn't supply the +required argument), and shows some basic help, with an instruction on how to get more help. + +Let's follow that instruction: + +```bash +./tutorial -Help +``` + +Now we get this output: + +```text Reads a file and displays the contents on the command line. Usage: tutorial [-Path] [-Help] [-Version] @@ -150,8 +167,8 @@ Usage: tutorial [-Path] [-Help] [-Version] > The actual usage help uses color if your console supports it. See [here](images/color.png) for > an example. -As you can see, the generated `Parse()` method lets us know what's wrong (we didn't supply the -required argument), and shows the usage help. +The generated `Parse()` method also took care of handling that `-Help` argument, and showed the +usage help. This usage help includes the description we applied to the class (this is the application description), and the `-Path` argument using the [`DescriptionAttribute`][]. This is how you can @@ -159,12 +176,11 @@ provide detailed information about your arguments to your users. It's strongly r always add a description to your arguments. You can also see that there are two more arguments that we didn't define: `-Help` and `-Version`. -These arguments are automatically added by Ookii.CommandLine. So, what do they do? +These arguments are automatically added by Ookii.CommandLine. -If you use the `-Help` argument (`./tutorial -Help`), it shows the same message as before. The only -difference is that there's no error message, even if you omitted the `-Path` argument. And even if -you do supply a path together with `-Help`, it still shows the help and exits, it doesn't run the -application. Basically, the presence of `-Help` will override anything else. +We've already seen what `-Help` does: it shows the usage help. Even if you supply other arguments +along with `-Help`, it will still show the help and exit; it doesn't run the application. Basically, +the presence of `-Help` will override anything else. The `-Version` argument shows version information about your application: @@ -178,8 +194,9 @@ copyright information, if there is any (there's not in this case). You can also `AssemblyTitleAttribute` or [`ApplicationFriendlyNameAttribute`][] attribute to specify a custom name instead of the assembly name. -> If you define an argument called "Help" or "Version", the automatic arguments won't be added. -> Also, you can disable the automatic arguments using the [`ParseOptionsAttribute`][] attribute. +> If you define your own argument called "Help" or "Version", the automatic arguments won't be added. +> Also, you can disable the automatic arguments using the [`ParseOptionsAttribute`][] attribute or +> [`ParseOptions`][] class. Note that the positional "Path" argument still has its name shown as `-Path`. That's because every argument, even positional ones, can still be supplied by name. So if you run this: @@ -278,22 +295,48 @@ Now we can run the application like this: And it'll only show the first five lines of the file, using black-on-white text. -If you supply a value that's not a valid integer for `-MaxLines`, or a value that's less than 1, -you'll once again get an error message and the usage help. +If you supply a value for `-MaxLines` that's not a valid integer, it shows an error message again: -What do you think will happen if we run this command? +```bash +./tutorial ../../../tutorial.csproj -lines hello +``` + +```text +The value 'hello' provided for argument 'MaxLines' could not be interpreted as a 'Number'. + +Usage: tutorial [-Path] [-Help] [-Inverted] [-MaxLines ] [-Version] + +Run 'tutorial -Help' for more information. +``` + +And because of the `ValidateRangeAttribute`, we can't specify a value less than 1 either. + +```bash +./tutorial ../../../tutorial.csproj -lines 0 +``` + +```text +The argument 'MaxLines' must be at least 1. + +Usage: tutorial [-Path] [-Help] [-Inverted] [-MaxLines ] [-Version] + +Run 'tutorial -Help' for more information. +``` + +Now, what do you think will happen if we run this command? ```text ./tutorial ../../../tutorial.csproj -m 5 -i ``` -If you tried it, you can see that it worked. By default, Ookii.CommandLine will treat any unique -prefix of a command line argument's name or aliases as an alias for that command. So, `-m` is -automatically an alias for `-MaxLines`. As is `-ma`, and `-max`, etc. And `-l` is as well, as it's -a prefix of the alias `-Lines`. +You might expect that to fail, as there are no arguments named `-m` or `-i`. However, if you tried +it, you can see that it worked. By default, Ookii.CommandLine will treat any unique prefix of a +command line argument's name or aliases as an alias for that argument. So, `-m` is automatically an +alias for `-MaxLines`. As is `-ma`, and `-max`, etc. And `-l` is as well, as it's a prefix of the +alias `-Lines`. > This only works if the prefix matches exactly one argument. And if you don't like this behavior, -> is can be disabled using the `ParseOptionsAttribute.AutoPrefixAliases` property. +> it can be disabled using the `ParseOptionsAttribute.AutoPrefixAliases` property. Let's take a look at the usage help for our updated application, by running `./tutorial -help`: @@ -324,6 +367,9 @@ applied, and we can see that the value, "Number", is used inside the angle brack the type of values the argument accepts. It defaults to the type name, but "Int32" might not be very meaningful to people who aren't programmers, so we've changed it to "Number" instead. +You may have noticed above that the value description was also used in the error message when we +provided an invalid value. + You can also see that the [`ValidateRangeAttribute`][] doesn't just validate its condition, it also adds that condition to the description of the argument (this can be disabled either globally or on a per-validator basis if you want). So you don't have to worry about keeping the description and @@ -348,10 +394,10 @@ public int MaxLines { get; set; } = 10; ``` > Instead of initializing the property, you can also use the -> [`CommandLineArgumentAttribute.DefaultValue`][] property, which can be useful if e.g. you're not using -> an automatic property (so you can't have a direct initializer like that). And, this method accepts -> not just the argument's actual type, but also any string that can be converted to it. For example, -> both `[CommandLineArgument(DefaultValue = 10)]` and `[CommandLineArgument(DefaultValue = "10")]` +> [`CommandLineArgumentAttribute.DefaultValue`][] property, which can be useful if e.g. you're not +> using an automatic property (so you can't have a direct initializer like that). And, that property +> accepts not just the argument's actual type, but also any string that can be converted to it. For +> example, both `[CommandLineArgument(DefaultValue = 10)]` and `[CommandLineArgument(DefaultValue = "10")]` > are equivalent to the above. Handy if your argument's type doesn't have literals. This default value would be shown in the usage help as well, similar to the validator: @@ -361,33 +407,39 @@ This default value would be shown in the usage help as well, similar to the vali The maximum number of lines to output. Must be at least 1. Default value: 10. ``` -## Long/short mode and other customizations +## POSIX conventions and other options Ookii.CommandLine offers many options to customize the way it parses the command line. For example, you can disable the use of white space as a separator between argument names and values, and specify -a custom separator. You can specify custom argument name prefixes, instead of `-` which is the +custom separators. You can specify custom argument name prefixes, instead of `-` which is the default (on Windows only, `/` is also accepted by default). You can make the argument names case sensitive. And there's more. -Most of these options can be specified using the [`ParseOptionsAttribute`][], which you can apply to -your class. Let's apply some options: +One thing you may want to do is use POSIX-like conventions, instead of the default PowerShell-like +parsing behavior. With POSIX conventions, arguments have separate long and short, one-character +names, which use different prefixes (typically `--` for long names and `-` for short). Argument +names are typically lowercase, with dashes between words, and are case sensitive. These are the same +conventions followed by tools such as `dotnet` or `git`, and many others. For a cross-platform +application, you may prefer these conventions over the default, but it's up to you of course. + +A convenient way to change these options is to use the [`ParseOptionsAttribute`][], which you can +apply to your class. Let's use it to enable POSIX mode: ```csharp +[GeneratedParser] [Description("Reads a file and displays the contents on the command line.")] -[ParseOptions(Mode = ParsingMode.LongShort, - CaseSensitive = true, - ArgumentNameTransform = NameTransform.DashCase, - ValueDescriptionTransform = NameTransform.DashCase)] -class Arguments +[ParseOptions(IsPosix = true)] +partial class Arguments { - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The path of the file to read.")] - public string? Path { get; set; } + public required string Path { get; set; } - [CommandLineArgument(IsShort = true, ValueDescription = "number")] + [CommandLineArgument(IsShort = true)] [Description("The maximum number of lines to output.")] + [ValueDescription("number")] [ValidateRange(1, null)] - [Alias("max")] + [Alias("lines")] public int? MaxLines { get; set; } [CommandLineArgument(IsShort = true)] @@ -396,19 +448,24 @@ class Arguments } ``` +The `ParseOptionsAttribute.IsPosix` property is actually a shorthand way to set several related +properties. The above attribute is identical to this: + +```csharp +[ParseOptions(Mode = ParsingMode.LongShort, + CaseSensitive = true, + ArgumentNameTransform = NameTransform.DashCase, + ValueDescriptionTransform = NameTransform.DashCase)] +``` + We've done a few things here: we've turned on an alternative set of parsing rules by setting the [`Mode`][Mode_2] property to [`ParsingMode.LongShort`][], we've made argument names case sensitive, and we've applied a name transformation to both argument names and value descriptions, which will make them lower case with dashes between words (e.g. "max-lines"). -These options combined make the application's parsing behavior very similar to common POSIX -conventions; the same conventions followed by tools such as `dotnet` or `git`, and many others. For -a cross-platform application, you may prefer these conventions over the default, but it's up to you -of course. - -Long/short mode is the key to this behavior. It allows every argument to have two separate names: -a long name, using the `--` prefix by default, and a single-character short name using the `-` -prefix (and `/` on Windows). +Long/short mode is the key to getting POSIX-like behavior. It allows every argument to have two +separate names: a long name, using the `--` prefix by default, and a single-character short name +using the `-` prefix (and `/` on Windows). When using long/short mode, all arguments have long names by default, but you'll need to indicate which arguments have short names. We've done that here with the `MaxLines` and `Inverted` @@ -444,8 +501,8 @@ Usage: tutorial [--path] [--help] [--inverted] [--max-lines ] [ -i, --inverted [] Use black text on a white background. - -m, --max-lines (--max) - The maximum number of lines to output. + -m, --max-lines (--lines) + The maximum number of lines to output. Must be at least 1. --version [] Displays version information. @@ -456,14 +513,9 @@ can see the result of the name transformation on all the arguments and value des the automatic `--help` and `--version` arguments, which are now also lower case. In addition to the [`ParseOptionsAttribute`][] attribute, you can also use the [`ParseOptions`][] -class to specify these and many other options, including where to write errors and help, and -customization options for the usage help. You can pass an instance of the [`ParseOptions`][] class -to the [`Parse()`][Parse()_1] method. - -For the options that are available on both the [`ParseOptionsAttribute`][] attribute and the -[`ParseOptions`][] class, you can choose which method to use based on your personal preference. If -you specify the same option in both the [`ParseOptionsAttribute`][] attribute and the -[`ParseOptions`][] class, the [`ParseOptions`][] class takes precedence. +class to specify these and many other options. [`ParseOptions`][] can also be used to customize +where to write errors and help, and to customize the usage help. You can pass an instance of the +[`ParseOptions`][] class to the generated `Parse()` method. ## Using subcommands From 9ad9d25848bfbb4cdc93cd96c77c78bde45d882e Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 14 Jun 2023 17:50:23 -0700 Subject: [PATCH 143/234] Finished tutorial updates. --- docs/Tutorial.md | 353 ++++++++++++++++++++--------------------------- 1 file changed, 146 insertions(+), 207 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index a0b57ec9..3bfcb62d 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -526,11 +526,12 @@ different, and needs its own command line arguments. Creating subcommands with Ookii.CommandLine is very similar to what we've been doing already. A subcommand is a class that defines arguments, same as before; the class will just have to implement -the [`ICommand`][] interface, and use the [`CommandAttribute`][] attribute. Additionally, we'll have -to change our `Main()` method to use subcommands. +the [`ICommand`][] interface, and use the [`CommandAttribute`][] attribute. And, instead of using +`Parse()` directly, we'll use the command manager. Let's change the example we've built so far to use subcommands. I'm going to continue with the -POSIX-like long/short mode, but if you prefer the defaults, you can go back to that version too. +POSIX-like long/short mode settings, but if you prefer the defaults, you can go back to that version +too. First, we'll add another `using` statement to Arguments.cs: @@ -538,25 +539,27 @@ First, we'll add another `using` statement to Arguments.cs: using Ookii.CommandLine.Commands; ``` -Then, we'll rename our `Arguments` class to `ReadCommand` (just for clarity), and change it into a -subcommand: +Then, we'll rename our `Arguments` class to `ReadCommand` (we'll use the class name to derive the +command name), and change it into a subcommand: ```csharp -[Command("read")] +[GeneratedParser] +[Command] [Description("Reads a file and displays the contents on the command line.")] -class ReadCommand : ICommand +partial class ReadCommand : ICommand ``` -We've added the [`CommandAttribute`][], which indicates the class is a command and lets us specify -the name of the command, which is "read" in this case. We've also added the [`ICommand`][] +We've added the [`CommandAttribute`][], which indicates the class is a command, and can also be used +to set an explicit name if you don't want to use the class name. We've also added the [`ICommand`][] interface, which all commands must implement. -Note that we've *removed* the [`ParseOptionsAttribute`][]. Don't worry, we'll add the options back -elsewhere later, so they'll apply to all commands. +Note that we've *removed* the [`ParseOptionsAttribute`][]. Options set with the attribute would +apply only to the command with the attribute, and usually you want to use the same options for all +commands. So, we'll set our options a different way further down. -We don't have to change anything about the properties defining the arguments. However, we do have -to implement the [`ICommand`][] interface, which has a single method called [`Run()`][Run()_1]. To implement it, we -move the implementation of `ReadFile()` from Program.cs into this method: +We don't have to change anything about the properties defining the arguments. However, we do have to +implement the [`ICommand`][] interface, which has a single method called [`Run()`][Run()_1]. To +implement it, we take the code from Program.cs and move it into this method: ```csharp public int Run() @@ -567,7 +570,7 @@ public int Run() Console.ForegroundColor = ConsoleColor.Black; } - var lines = File.ReadLines(Path!); + var lines = File.ReadLines(Path); if (MaxLines is int maxLines) { lines = lines.Take(maxLines); @@ -591,60 +594,87 @@ The [`Run()`][Run()_1] method is like the `Main()` method for your command, and treated like the exit code returned from `Main()`, because typically, you will return the executed command's return value from `Main()`. -And that's it: we've now defined a command. However, we still need to change the `Main()` method to -use commands instead of just parsing arguments from a single class. Fortunately, this is very -simple. First add the `using Ookii.CommandLine.Commands;` statement to Program.cs, and then update -your `Main()` method: +And that's it: we've now defined a command. However, we still need to change the application to +use commands instead of just parsing arguments from a single class. To do this, we'll use the +`CommandManager` class. + +First, we'll add a file named GeneratedManager.cs, with these contents: ```csharp -public static int Main() +using Ookii.CommandLine.Commands; + +namespace Tutorial; + +[GeneratedCommandManager] +partial class GeneratedManager { - var options = new CommandOptions() - { - Mode = ParsingMode.Default, - ArgumentNameComparer = StringComparer.InvariantCulture, - ArgumentNameTransform = NameTransform.DashCase, - ValueDescriptionTransform = NameTransform.DashCase, - }; - - var manager = new CommandManager(options); - return manager.RunCommand() ?? 1; } ``` -The [`CommandManager`][] class handles finding your commands, and lets you specify various options. -The default constructor will look for subcommand classes in the calling assembly. +The `GeneratedCommandManagerAttribute` is similar to the `GeneratedParserAttribute`, except it turns +the target class into a command manager. The `GeneratedCommandManagerAttribute` will make your class +derive from `CommandManager`, and generates code to find and instantiate the commands in this +assembly. + +> You can also use `CommandManager` directly, without a generated class, in which case reflection +> is used to find the commands. Do this if you can't use [source generation](SourceGeneration.md). + +Now replace the code in Program.cs with the following. + +```csharp +using Ookii.CommandLine.Commands; +using Tutorial; + +var options = new CommandOptions() +{ + IsPosix = true, +}; + +var manager = new GeneratedManager(options); +return manager.RunCommand() ?? 1; +``` + +That's all you need to do to find, parse arguments for, and run any command in your application. + +Here, we use the [`CommandOptions`][] to set the same options as before, so they'll apply to every +command (even if currently we have only one command). The [`CommandOptions`][] class derives from +the [`ParseOptions`][] class, so it can be used to specify all the same options, in addition to +some that are specific to commands. -The [`CommandOptions`][] class derives from the [`ParseOptions`][] class, so it can be used to -specify all the same options, and these will be shared by every command. We've used this to apply -the options that we were previously setting using the [`ParseOptionsAttribute`][]. +Actually, for [`CommandOptions`][] the meaning of `IsPosix` is slightly different. It sets the same +options as before, but also sets two additional ones. It's actually equivalent to the following: -You could of course still use the [`ParseOptionsAttribute`][], but if you do, those options only -apply to that particular command, so for consistency between your commands using the -[`CommandOptions`][] class is often better. +```csharp +var options = new CommandOptions() +{ + Mode = ParsingMode.LongShort, + ArgumentNameComparison = StringComparison.InvariantCulture, + ArgumentNameTransform = NameTransform.DashCase, + ValueDescriptionTransform = NameTransform.DashCase, + CommandNameComparison = StringComparison.InvariantCulture, + CommandNameTransform = NameTransform.DashCase, +}; +``` -Note that the [`ParseOptions`][] (and therefore, the [`CommandOptions`][]) class has no -[`CaseSensitive`][] property; instead, you have to set the -[`ArgumentNameComparer`][ArgumentNameComparer_1] property. We use -[`StringComparer.InvariantCulture`][] here to get case-sensitive argument names. +So in addition to enabling what it did before, it also made command names case sensitive (they are +case insensitive by default, just like argument names) and transforms their names to lowercase +separated by dashes as well. -> For the default case-insensitive behavior, [`StringComparer.OrdinalIgnoreCase`][] is used. You can -> also use [`StringComparer.Ordinal`][] for case sensitivity, but [`StringComparer.InvariantCulture`] -> has better sorting for the usage help if you mix upper and lower case argument names. +> Note that [`ParseOptions`][], and by extension [`CommandOptions`][], use a `StringComparison` +> value instead of just a [`CaseSensitive`][] property. -The [`RunCommand()`][] method will take the arguments from [`Environment.GetCommandLineArgs()`][] (as -before, you can also pass them explicitly), and uses the first argument as the command name. If a -command with that name exists, it uses [`CommandLineParser`][] to parse the arguments for that command, -and finally invokes the [`ICommand.Run()`][] method. If anything goes wrong, it will either display a -list of commands, or if a command has been found, the help for that command. The return value is the -value returned from [`ICommand.Run()`][], or null if parsing failed, in which case we return a non-zero -exit code to indicate failure. +The [`RunCommand()`][] method will take the arguments from [`Environment.GetCommandLineArgs()`][] +(as before, you can also pass them explicitly), and uses the first argument as the command name. If +a command with that name exists, it uses [`CommandLineParser`][] to parse the arguments for that +command, and finally invokes the [`ICommand.Run()`][] method. If anything goes wrong, it will either +display a list of commands, or if a command has been found, the help for that command. The return +value is the value returned from [`ICommand.Run()`][], or null if parsing failed, in which case we +return a non-zero exit code to indicate failure. > If you want to customize any of these steps, there are methods like [`GetCommand()`][] and > [`CreateCommand()`][] that you can call to do this manually. -If we build our application, and run it without arguments again (`./tutorial`), we see the -following: +If we build our application, and run it without arguments (`./tutorial`), we see the following: ```text Usage: tutorial [arguments] @@ -656,19 +686,26 @@ The following commands are available: version Displays version information. + +Run 'tutorial --help' for more information about a command. ``` When no command, or an unknown command, is supplied, a list of commands is printed. The [`DescriptionAttribute`][] for our class, which was the application description before, is now the description of the command. +But why is the command called `read`, and not `read-command`, if it's based on the class name +`ReadCommand`? If you use a name transformation for command names, it will strip the suffix +"Command" from the name by default. Use the `CommandOptions.StripCommandNameSuffix` property to +customize that behavior. + There is a second command, `version`, which is automatically added unless there already is a command with that name. It does the same thing as the `-Version` argument before. Let's see the usage help for our command: ```text -./tutorial read -help +./tutorial read --help ``` Which gives the following output: @@ -687,7 +724,7 @@ Usage: tutorial read [--path] [--help] [--inverted] [--max-lines ] Use black text on a white background. - -m, --max-lines (--max) + -m, --max-lines (--lines) The maximum number of lines to output. Must be at least 1. ``` @@ -695,67 +732,33 @@ There are two differences to spot from the earlier version: the usage syntax now before the arguments, indicating you have to use the command, and there is no automatic `--version` argument, since that would be redundant with the `version` command. -## Command options +## Adding an application description -We already used the [`CommandOptions`][] class to set some options relating to the argument parsing -behavior of the commands, but there are also several options that apply to commands directly. +The usage help for the single arguments class would print an application description at the top, +but the command list doesn't have anything like that. We can, however, add it. -Let's change our main method to add some more options: +To do, make the following change to the [`CommandOptions`][] (and add `using Ookii.CommandLine` at +the top of the file): ```csharp var options = new CommandOptions() { - Mode = ParsingMode.Default, - ArgumentNameComparer = StringComparer.InvariantCulture, - ArgumentNameTransform = NameTransform.DashCase, - ValueDescriptionTransform = NameTransform.DashCase, - CommandNameTransform = NameTransform.DashCase, - CommandNameComparer = StringComparer.InvariantCulture, + IsPosix = true, UsageWriter = new UsageWriter() { - IncludeCommandHelpInstruction = true, IncludeApplicationDescriptionBeforeCommandList = true, - }, + } }; - -var manager = new CommandManager(options); -return manager.RunCommand() ?? 1; -``` - -The first new option applies a name transformation to the command names if no explicit name is -specified, similar to the argument name transformation we used earlier. This means we can change our -class to this: - -```csharp -[Command] -[Description("Reads a file and displays the contents on the command line.")] -class ReadCommand : ICommand ``` -We've removed the explicit name from the [`CommandAttribute`][]. If you run the application, you'll -see the command is still called "read". That's because for subcommands, the name transformation will -strip the suffix "Command" from the name by default. This too can be customized with the -[`CommandOptions`][] class. - -Next, we've set a [`CommandNameComparer`][] to make the command names case sensitive as well (the -default is case sensitive). - -We also set some options to customize the usage help. The first one is the -[`IncludeCommandHelpInstruction`][] property, which causes the [`CommandManager`][] to print a -message like `Run 'tutorial --help' for more information about a command.` after the -command list. This is disabled by default because the [`CommandManager`][] won't check if all the -commands actually have a `--help` argument. It's recommended to enable this if all your commands do. - -> The help argument name is automatically adjusted based on the parsing mode and name transformation, -> so if you use the default mode, it'll say `-Help` instead. - -The last option is [`IncludeApplicationDescriptionBeforeCommandList`][], which prints the assembly -description before the command list. However, if you run your application, you'll see it didn't do -anything. That's because the tutorial application doesn't have an assembly description. Insert the -following into a `` in the tutorial.csproj file to fix that. +We've set the [`IncludeApplicationDescriptionBeforeCommandList`][] option, which prints the assembly +description before the command list. So to set a description, we'll add one in the tutorial.csproj +file. ```xml -An application to read and write files. + + An application to read and write files. + ``` Now, if you run the application without arguments, you'll see this: @@ -776,10 +779,6 @@ The following commands are available: Run 'tutorial --help' for more information about a command. ``` -So we have an application description, and instructions for the user on how to get help for a -command. But, we still have only one command ("version" doesn't count), and the description we just -added is lying (the application only reads files). - ## Multiple commands An application with only one subcommand doesn't really need to use subcommands, so let's add a @@ -792,17 +791,18 @@ using System.ComponentModel; namespace Tutorial; +[GeneratedParser] [Command] [Description("Writes text to a file.")] -class WriteCommand : ICommand +partial class WriteCommand : ICommand { - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The path of the file to write.")] - public string? Path { get; set; } + public required string Path { get; set; } - [CommandLineArgument(Position = 1, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("The text to write to the file.")] - public string[]? Text { get; set; } + public required string[] Text { get; set; } [CommandLineArgument(IsShort = true)] [Description("Append to the file instead of overwriting it.")] @@ -812,11 +812,11 @@ class WriteCommand : ICommand { if (Append) { - File.AppendAllLines(Path!, Text!); + File.AppendAllLines(Path, Text); } else { - File.WriteAllLines(Path!, Text!); + File.WriteAllLines(Path, Text); } return 0; @@ -827,8 +827,7 @@ class WriteCommand : ICommand There's one thing here that we haven't seen before, and that's a multi-value argument. The `--text` argument has an array type (`string[]`), which means it can have multiple values by supplying it multiple times. We could, for example, use `--text foo --text bar` to assign the values "foo" and -"bar" to it. Because it's also a positional argument, we can also simply use `foo bar` to do the -same. +"bar" to it. Because it's also a positional argument, we can simply use `foo bar` to do the same. > A positional multi-value argument must always be the last positional argument. @@ -859,7 +858,7 @@ The following commands are available: write Writes text to a file. -Run 'tutorial -Help' for more information about a command. +Run 'tutorial --help' for more information about a command. ``` As you can see, our application picked up the new command without us needing to do anything. That's @@ -887,14 +886,10 @@ Usage: tutorial write [--path] [--text] ... [--append] [--help] We can test out our new command like this: -```text -$ ./tutorial write test.txt "Hello!" "Ookii.CommandLine is pretty neat." "At least I think so." -$ ./tutorial write test.txt "Thanks for using it!" -a -$ ./tutorial read test.txt -Hello! -Ookii.CommandLine is pretty neat. -At least I think so. -Thanks for using it! +```bash +./tutorial write test.txt "Hello!" "Ookii.CommandLine is pretty neat." "At least I think so." +./tutorial write test.txt "Thanks for using it!" -a +./tutorial read test.txt ``` Here, we wrote three lines of text to a file, then appended one more line, and read them back using @@ -904,23 +899,23 @@ the "read" command. If you want to use asynchronous code in your application, subcommands provide a way to do that too. -To make a command asynchronous, we have to implement the [`IAsyncCommand`][] interface. This interface -derives from the [`ICommand`][] interface, and adds a [`RunAsync()`][RunAsync()_1] method for you to implement. Then, -you can invoke your command using the [`CommandManager.RunCommandAsync()`][] method. +To make a command asynchronous, we have to implement the [`IAsyncCommand`][] interface. This +interface derives from the [`ICommand`][] interface, and adds a [`RunAsync()`][RunAsync()_1] method +for you to implement. Then, you can invoke your command using the +[`CommandManager.RunCommandAsync()`][] method. -Let's make the `WriteCommand` asynchronous. When you do this, you typically only care about the -[`RunAsync()`][RunAsync()_1] method, but since [`IAsyncCommand`][] derives from [`ICommand`][], you must still provide a -[`Run()`][Run()_1] method. You could just leave it empty (or throw an exception), since [`RunCommandAsync()`][] -will never call it. An easier way is to derive your command from the [`AsyncCommandBase`][] class, which -provides a default implementation of the [`Run()`][Run()_0] method that will invoke [`RunAsync()`][RunAsync()_1] and wait for -it to finish. +Because you still have to implement [`Run()`][Run()_1] when you use the [`IAsyncCommand`][] +interface, Ookii.CommandLine also provides the [`AsyncCommandBase`][] class for convenience, which +provides a default implementation of the [`Run()`][Run()_0] method that will invoke +[`RunAsync()`][RunAsync()_1] and wait for it to finish. So, we'll make the following changes to `WriteCommand`: ```csharp +[GeneratedParser] [Command] [Description("Writes text to a file.")] -class WriteCommand : AsyncCommandBase +partial class WriteCommand : AsyncCommandBase { /* Properties are unchanged */ @@ -928,11 +923,11 @@ class WriteCommand : AsyncCommandBase { if (Append) { - await File.AppendAllLinesAsync(Path!, Text!); + await File.AppendAllLinesAsync(Path, Text); } else { - await File.WriteAllLinesAsync(Path!, Text!); + await File.WriteAllLinesAsync(Path, Text); } return 0; @@ -940,82 +935,34 @@ class WriteCommand : AsyncCommandBase } ``` -If you build and run your application now, you'll find that it works, despite not calling -[`RunCommandAsync()`][] yet. That's because [`RunCommand()`][] will invoke [`AsyncCommandBase.Run()`][], which -will create a task to run [`RunAsync()`][RunAsync()_0] and wait for it. +If you build and run your application now, you'll find that it works, because of the +[`AsyncCommandBase.Run()`][] method. -However, to fully take advantage of asynchronous tasks, you'll want to update the `Main()` method -as follows: +However, to fully take advantage of asynchronous tasks, you'll want to replace the +[`RunCommand()`][] method call with [`RunCommandAsync()`][] in Program.cs: ```csharp -public static async Task Main() -{ - var options = new CommandOptions() - { - Mode = ParsingMode.Default, - ArgumentNameComparer = StringComparer.InvariantCulture, - ArgumentNameTransform = NameTransform.DashCase, - ValueDescriptionTransform = NameTransform.DashCase, - CommandNameTransform = NameTransform.DashCase, - CommandNameComparer = StringComparer.InvariantCulture, - UsageWriter = new UsageWriter() - { - IncludeCommandHelpInstruction = true, - IncludeApplicationDescriptionBeforeCommandList = true, - }, - }; - - var manager = new CommandManager(options); - return await manager.RunCommandAsync() ?? 1; -} +return await manager.RunCommandAsync() ?? 1; ``` You'll notice that even with this change, the "read" command still works, despite not being -asynchronous. That's because the [`RunCommandAsync()`][] will check if a command implements -[`IAsyncCommand`][], and if it doesn't, it will fall back to just calling [`ICommand.Run()`][]. So you can -choose for each command to make it asynchronous or not according to its needs. +asynchronous. That's because the [`RunCommandAsync()`][] supports both synchronous and asynchronous +commands, so you can mix and match them as you please. Converting `ReadCommand` to use asynchronous code is left as an exercise to the reader (hint: you'll -need .Net 7 for [`File.ReadLinesAsync()`][], and the -[`System.Linq.Async`](https://www.nuget.org/packages/System.Linq.Async) package to be able to use -the [`Take()`][] extension method on [`IAsyncEnumerable`][]; or you can just use [`StreamReader`][]). +need the [`System.Linq.Async`](https://www.nuget.org/packages/System.Linq.Async) package to be able +to use the [`Take()`][] extension method on the [`IAsyncEnumerable`][] returned by +[`File.ReadLinesAsync()`][]). ## Common arguments for commands Sometimes, you'll want some arguments to be available to all commands. With Ookii.CommandLine, the way to do this is to make a common base class. [`CommandLineParser`][] will consider base class members -when determining what arguments are available. - -For example, if we wanted to make a common base class to share the `--path` argument between the -`read` and `write` commands, we could do so like this: - -```csharp -abstract class BaseCommand : AsyncCommandBase -{ - [CommandLineArgument(Position = 0, IsRequired = true)] - [Description("The path of the file.")] - public string? Path { get; set; } -} - -[Command] -class ReadCommand : BaseCommand -{ - /* Remove the Path property, leave everything else */ -} - -[Command] -class WriteCommand : BaseCommand -{ - /* Remove the Path property, leave everything else */ -} -``` - -Now both commands share the `--path` argument defined in the base class, in addition to the arguments -they define themselves. Note that `BaseCommand` is not itself a command, because it doesn't have the -[`CommandAttribute`][] attribute (and also because it's `abstract`). +when determining what arguments are available. For example, here we could move the `--path` argument +to a common base class. -If you apply a [`ParseOptionsAttribute`][] attribute to the `BaseCommand` class, you can also share -parse options between multiple commands, without having to use [`CommandOptions`][] to do so. +For more information on how to do this, see the +[documentation on subcommand base classes](Subcommands.md#multiple-commands-with-common-arguments). ## More information @@ -1039,7 +986,6 @@ following resources: [`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser.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 -[`CommandNameComparer`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameComparer.htm [`CommandOptions`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_CommandOptions.htm [`CreateCommand()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute @@ -1052,24 +998,17 @@ following resources: [`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 [`IncludeApplicationDescriptionBeforeCommandList`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList.htm -[`IncludeCommandHelpInstruction`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_IncludeCommandHelpInstruction.htm [`Nullable`]: https://learn.microsoft.com/dotnet/api/system.nullable-1 [`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 [`ParsingMode.LongShort`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParsingMode.htm [`RunCommand()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm [`RunCommandAsync()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm -[`StreamReader`]: https://learn.microsoft.com/dotnet/api/system.io.streamreader -[`StringComparer.InvariantCulture`]: https://learn.microsoft.com/dotnet/api/system.stringcomparer.invariantculture -[`StringComparer.Ordinal`]: https://learn.microsoft.com/dotnet/api/system.stringcomparer.ordinal -[`StringComparer.OrdinalIgnoreCase`]: https://learn.microsoft.com/dotnet/api/system.stringcomparer.ordinalignorecase [`Take()`]: https://learn.microsoft.com/dotnet/api/system.linq.enumerable.take [`Uri`]: https://learn.microsoft.com/dotnet/api/system.uri [`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm -[ArgumentNameComparer_1]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparer.htm [Mode_2]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_Mode.htm [Parse()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm [Run()_0]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm [Run()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm -[RunAsync()_0]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_RunAsync.htm [RunAsync()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync.htm From 3a17aeb6a1d86e0aff51afa06e39293273b14ad3 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 14 Jun 2023 17:52:05 -0700 Subject: [PATCH 144/234] Fixed code block language. --- docs/Tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 3bfcb62d..0fc9ba1a 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -836,7 +836,7 @@ file, optionally appending to the file. Let's build and run our application again, without arguments: -```text +```bash ./tutorial ``` From 3e488c85478735bd54e2f9e95cf2d61b4e03035a Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 15 Jun 2023 14:17:19 -0700 Subject: [PATCH 145/234] XML comments to make SHFB happy. --- docs/Ookii.CommandLine.shfbproj | 15 ++-- .../Support/CommandProvider.cs | 4 +- .../Support/GeneratedArgument.cs | 76 ++++++++++++------- .../Support/GeneratedCommandInfo.cs | 16 ++-- .../GeneratedCommandInfoWithCustomParsing.cs | 14 +++- 5 files changed, 79 insertions(+), 46 deletions(-) diff --git a/docs/Ookii.CommandLine.shfbproj b/docs/Ookii.CommandLine.shfbproj index 45fd5118..b459dc27 100644 --- a/docs/Ookii.CommandLine.shfbproj +++ b/docs/Ookii.CommandLine.shfbproj @@ -27,19 +27,24 @@ <para> Provides functionality for defining and parsing command line arguments, and for generating usage help. </para> - <para> +<para> Provides functionality for creating applications with multiple subcommands, each with their own arguments. </para> - <para> +<para> Provides helpers for supporting virtual terminal sequences and color output. </para> - <para> +<para> Provides attributes used to validate the value of arguments, and the relation between arguments. </para> - +<para> + Provides functionality for converting strings to the actual type of the 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++ diff --git a/src/Ookii.CommandLine/Support/CommandProvider.cs b/src/Ookii.CommandLine/Support/CommandProvider.cs index f4a21e3d..e4616298 100644 --- a/src/Ookii.CommandLine/Support/CommandProvider.cs +++ b/src/Ookii.CommandLine/Support/CommandProvider.cs @@ -30,8 +30,8 @@ public abstract class CommandProvider public abstract IEnumerable GetCommandsUnsorted(CommandManager manager); /// - /// Gets the application description + /// 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 index 9343073f..d7eb12af 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -33,36 +33,54 @@ private GeneratedArgument(ArgumentInfo info, Action? setPropert } /// - /// This class is for internal use by the source generator, and should not be used in your code. + /// 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, diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs index 85c72f6d..cb9d2456 100644 --- a/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs @@ -16,15 +16,15 @@ public class GeneratedCommandInfo : CommandInfo private readonly Func? _createParser; /// - /// This class is for internal use by the source generator, and should not be used in your code. + /// 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, diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs index fd6d5b8d..ad6f0160 100644 --- a/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs @@ -5,11 +5,21 @@ namespace Ookii.CommandLine.Support; -/// +/// +/// This class is for internal use by the source generator, and should not be used in your code. +/// +/// The command class. 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, From de399aa3cccb804fcbc1e335aaff68070c9b2cc2 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 15 Jun 2023 15:06:48 -0700 Subject: [PATCH 146/234] Clean up API refs. --- docs/refs.json | 61 +++----------------------------------------------- 1 file changed, 3 insertions(+), 58 deletions(-) diff --git a/docs/refs.json b/docs/refs.json index 5aa8fb78..0787ae65 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -1,19 +1,10 @@ { - "#apiPrefix": "https://learn.microsoft.com/dotnet/api/", - "#prefix": "https://www.ookii.org/docs/commandline-3.1/html/", - "#suffix": ".htm", - "AddCommand": null, + "#apiPrefix": null, + "#prefix": null, + "#suffix": null, "AliasAttribute": "T_Ookii_CommandLine_AliasAttribute", "AllowDuplicateDictionaryKeysAttribute": "T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute", "ApplicationFriendlyNameAttribute": "T_Ookii_CommandLine_ApplicationFriendlyNameAttribute", - "ArgumentNameAttribute": "T_Ookii_CommandLine_ArgumentNameAttribute", - "ArgumentNameAttribute.IsLong": "P_Ookii_CommandLine_ArgumentNameAttribute_IsLong", - "ArgumentNameAttribute.IsShort": "P_Ookii_CommandLine_ArgumentNameAttribute_IsShort", - "ArgumentNameAttribute.ShortName": "P_Ookii_CommandLine_ArgumentNameAttribute_ShortName", - "ArgumentNameComparer": [ - "P_Ookii_CommandLine_CommandLineParser_ArgumentNameComparer", - "P_Ookii_CommandLine_ParseOptions_ArgumentNameComparer" - ], "ArgumentParsed": "E_Ookii_CommandLine_CommandLineParser_ArgumentParsed", "Arguments": [ "P_Ookii_CommandLine_CommandLineParser_Arguments", @@ -25,13 +16,10 @@ "ArgumentValidationWithHelpAttribute": "T_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute", "AsyncCommandBase": "T_Ookii_CommandLine_Commands_AsyncCommandBase", "AsyncCommandBase.Run()": "M_Ookii_CommandLine_Commands_AsyncCommandBase_Run", - "Bar": null, - "BaseCommand": null, "CancelParsing": [ "P_Ookii_CommandLine_CommandLineArgument_CancelParsing", "P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing" ], - "CanConvertFrom()": "M_Ookii_CommandLine_TypeConverterBase_1_CanConvertFrom", "CaseSensitive": "P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive", "Category": "P_Ookii_CommandLine_CommandLineArgumentException_Category", "ClassValidationAttribute": "T_Ookii_CommandLine_Validation_ClassValidationAttribute", @@ -47,12 +35,10 @@ "CommandLineArgumentAttribute.IsShort": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort", "CommandLineArgumentAttribute.Position": "P_Ookii_CommandLine_CommandLineArgumentAttribute_Position", "CommandLineArgumentAttribute.ShortName": "P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName", - "CommandLineArgumentAttribute.ValueDescription": "P_Ookii_CommandLine_CommandLineArgumentAttribute_ValueDescription", "CommandLineArgumentErrorCategory": "T_Ookii_CommandLine_CommandLineArgumentErrorCategory", "CommandLineArgumentErrorCategory.ValidationFailed": "T_Ookii_CommandLine_CommandLineArgumentErrorCategory", "CommandLineArgumentException": "T_Ookii_CommandLine_CommandLineArgumentException", "CommandLineArgumentException.Category": "P_Ookii_CommandLine_CommandLineArgumentException_Category", - "CommandLineConstructorAttribute": "T_Ookii_CommandLine_CommandLineConstructorAttribute", "CommandLineParser": "T_Ookii_CommandLine_CommandLineParser", "CommandLineParser.GetUsage()": "M_Ookii_CommandLine_CommandLineParser_GetUsage", "CommandLineParser.HelpRequested": "P_Ookii_CommandLine_CommandLineParser_HelpRequested", @@ -61,11 +47,9 @@ "M_Ookii_CommandLine_CommandLineParser_Parse", "Overload_Ookii_CommandLine_CommandLineParser_Parse" ], - "CommandLineParser.Parse()": null, "CommandLineParser.Parse()": "M_Ookii_CommandLine_CommandLineParser_Parse__1", "CommandLineParser.ParseResult": "P_Ookii_CommandLine_CommandLineParser_ParseResult", "CommandLineParser.WriteUsage()": "M_Ookii_CommandLine_CommandLineParser_WriteUsage", - "CommandLineParser.WriteUsageToConsole()": null, "CommandLineParser": "T_Ookii_CommandLine_CommandLineParser_1", "CommandLineParser.Parse()": "Overload_Ookii_CommandLine_CommandLineParser_1_Parse", "CommandLineParser.ParseWithErrorHandling()": "M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling", @@ -73,22 +57,17 @@ "CommandManager.GetCommand()": "M_Ookii_CommandLine_Commands_CommandManager_GetCommand", "CommandManager.ParseResult": "P_Ookii_CommandLine_Commands_CommandManager_ParseResult", "CommandManager.RunCommandAsync()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync", - "CommandNameComparer": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameComparer", "CommandNameTransform": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform", "CommandOptions": "T_Ookii_CommandLine_Commands_CommandOptions", "CommandOptions.AutoVersionCommand": "P_Ookii_CommandLine_Commands_CommandOptions_AutoVersionCommand", "CommandOptions.CommandFilter": "P_Ookii_CommandLine_Commands_CommandOptions_CommandFilter", "CommandOptions.CommandNameTransform": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform", "CommandOptions.StripCommandNameSuffix": "P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix", - "ConnectionString": null, "Console.WindowWidth": "#system.console.windowwidth", - "ConvertFrom()": "M_Ookii_CommandLine_TypeConverterBase_1_ConvertFrom", "CreateCommand()": "Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand", - "CreateShellCommandOptions": null, "CultureInfo": "#system.globalization.cultureinfo", "CultureInfo.InvariantCulture": "#system.globalization.cultureinfo.invariantculture", "CurrentCulture": "#system.globalization.cultureinfo.currentculture", - "DatabaseCommand": null, "DateOnly": "#system.dateonly", "DateTime": "#system.datetime", "DayOfWeek": "#system.dayofweek", @@ -98,7 +77,6 @@ "P_Ookii_CommandLine_CommandLineArgument_DefaultValue", "P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue" ], - "DeleteCommand": null, "DescriptionAttribute": "#system.componentmodel.descriptionattribute", "DescriptionListFilterMode.Information": "T_Ookii_CommandLine_DescriptionListFilterMode", "Dictionary": "#system.collections.generic.dictionary-2", @@ -123,7 +101,6 @@ "M_Ookii_CommandLine_LineWrappingTextWriter_Flush", "Overload_Ookii_CommandLine_LineWrappingTextWriter_Flush" ], - "Foo": null, "GetArgument": "M_Ookii_CommandLine_CommandLineParser_GetArgument", "GetCommand()": "M_Ookii_CommandLine_Commands_CommandManager_GetCommand", "GetErrorMessage()": "M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetErrorMessage", @@ -149,12 +126,8 @@ ], "Indent": "P_Ookii_CommandLine_LineWrappingTextWriter_Indent", "Int32": "#system.int32", - "Inverted": null, "IsValid()": "M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsValid", - "KeyTypeConverterAttribute": "T_Ookii_CommandLine_KeyTypeConverterAttribute", "KeyValuePair": "#system.collections.generic.keyvaluepair-2", - "KeyValuePairConverter": "T_Ookii_CommandLine_KeyValuePairConverter_2", - "KeyValueSeparatorAttribute": "T_Ookii_CommandLine_KeyValueSeparatorAttribute", "LineWrappingTextWriter": "T_Ookii_CommandLine_LineWrappingTextWriter", "LineWrappingTextWriter.ForConsoleError()": "M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleError", "LineWrappingTextWriter.ForConsoleOut()": "M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleOut", @@ -164,10 +137,7 @@ "LineWrappingTextWriter.Wrapping": "P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping", "List": "#system.collections.generic.list-1", "List": "#system.collections.generic.list-1", - "LoadPlugins()": null, "LocalizedStringProvider": "T_Ookii_CommandLine_LocalizedStringProvider", - "Main()": null, - "MaxLines": null, "Mode": [ "P_Ookii_CommandLine_CommandLineParser_Mode", "P_Ookii_CommandLine_ParseOptions_Mode", @@ -185,7 +155,6 @@ "NameTransform.None": "T_Ookii_CommandLine_NameTransform", "Nullable": "#system.nullable-1", "Nullable": "#system.nullable-1", - "NullableConverter": "#system.componentmodel.nullableconverter", "NullArgumentValue": [ "T_Ookii_CommandLine_CommandLineArgumentErrorCategory", "M_Ookii_CommandLine_LocalizedStringProvider_NullArgumentValue" @@ -208,19 +177,16 @@ ], "ParseOptions": "T_Ookii_CommandLine_ParseOptions", "ParseOptions.AllowWhiteSpaceValueSeparator": "P_Ookii_CommandLine_ParseOptions_AllowWhiteSpaceValueSeparator", - "ParseOptions.ArgumentNameComparer": "P_Ookii_CommandLine_ParseOptions_ArgumentNameComparer", "ParseOptions.ArgumentNameTransform": "P_Ookii_CommandLine_ParseOptions_ArgumentNameTransform", "ParseOptions.AutoVersionArgument": "P_Ookii_CommandLine_ParseOptions_AutoVersionArgument", "ParseOptions.Culture": "P_Ookii_CommandLine_ParseOptions_Culture", "ParseOptions.DefaultValueDescriptions": "P_Ookii_CommandLine_ParseOptions_DefaultValueDescriptions", - "ParseOptions.NameValueSeparator": "P_Ookii_CommandLine_ParseOptions_NameValueSeparator", "ParseOptions.ShowUsageOnError": "P_Ookii_CommandLine_ParseOptions_ShowUsageOnError", "ParseOptions.StringProvider": "P_Ookii_CommandLine_ParseOptions_StringProvider", "ParseOptions.UsageWriter": "P_Ookii_CommandLine_ParseOptions_UsageWriter", "ParseOptionsAttribute": "T_Ookii_CommandLine_ParseOptionsAttribute", "ParseOptionsAttribute.AllowWhiteSpaceValueSeparator": "P_Ookii_CommandLine_ParseOptionsAttribute_AllowWhiteSpaceValueSeparator", "ParseOptionsAttribute.CaseSensitive": "P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive", - "ParseOptionsAttribute.NameValueSeparator": "P_Ookii_CommandLine_ParseOptionsAttribute_NameValueSeparator", "ParseResult.ArgumentName": "P_Ookii_CommandLine_ParseResult_ArgumentName", "ParseResult.LastException": "P_Ookii_CommandLine_ParseResult_LastException", "ParseResult.Status": "P_Ookii_CommandLine_ParseResult_Status", @@ -235,10 +201,6 @@ ], "ParsingMode.LongShort": "T_Ookii_CommandLine_ParsingMode", "ProhibitsAttribute": "T_Ookii_CommandLine_Validation_ProhibitsAttribute", - "ReadCommand": null, - "ReadDirectoryCommand": null, - "ReadFile": null, - "ReadFile()": null, "RequiresAnyAttribute": "T_Ookii_CommandLine_Validation_RequiresAnyAttribute", "RequiresAttribute": "T_Ookii_CommandLine_Validation_RequiresAttribute", "ResetIndent()": "M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent", @@ -247,22 +209,15 @@ "M_Ookii_CommandLine_Commands_AsyncCommandBase_Run", "M_Ookii_CommandLine_Commands_ICommand_Run" ], - "Run(Async)": null, "RunAsync()": [ "M_Ookii_CommandLine_Commands_AsyncCommandBase_RunAsync", "M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync" ], "RunCommand": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand", "RunCommand()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand", - "RunCommand(Async)": null, "RunCommandAsync()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync", "SetConsoleMode": "https://learn.microsoft.com/windows/console/setconsolemode", - "ShellCommand": null, - "ShellCommand.ExitCode": null, - "ShellCommandAttribute": null, - "ShellCommandAttribute.CustomParsing": null, "ShortAliasAttribute": "T_Ookii_CommandLine_ShortAliasAttribute", - "SomeName": null, "SortedDictionary": "#system.collections.generic.sorteddictionary-2", "StreamReader": "#system.io.streamreader", "String": "#system.string", @@ -272,16 +227,10 @@ "StringWriter": "#system.io.stringwriter", "StripCommandNameSuffix": "P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix", "System.ComponentModel.DescriptionAttribute": "#system.componentmodel.descriptionattribute", - "System.ComponentModel.TypeConverterAttribute": "#system.componentmodel.typeconverterattribute", - "T": null, "Take()": "#system.linq.enumerable.take", - "TERM": null, "TextFormat": "T_Ookii_CommandLine_Terminal_TextFormat", "TextWriter": "#system.io.textwriter", "ToString()": "#system.object.tostring", - "TypeConverter": "#system.componentmodel.typeconverter", - "TypeConverterAttribute": "#system.componentmodel.typeconverterattribute", - "TypeConverterBase": "T_Ookii_CommandLine_TypeConverterBase_1", "Uri": "#system.uri", "UsageHelpRequest.SyntaxOnly": "T_Ookii_CommandLine_UsageHelpRequest", "UsageWriter": "T_Ookii_CommandLine_UsageWriter", @@ -301,14 +250,12 @@ "ValidatePatternAttribute": "T_Ookii_CommandLine_Validation_ValidatePatternAttribute", "ValidatePatternAttribute.ErrorMessage": "P_Ookii_CommandLine_Validation_ValidatePatternAttribute_ErrorMessage", "ValidateRangeAttribute": "T_Ookii_CommandLine_Validation_ValidateRangeAttribute", - "ValidateSetAttribute": null, "ValidateStringLengthAttribute": "T_Ookii_CommandLine_Validation_ValidateStringLengthAttribute", "ValidationFailed": [ "M_Ookii_CommandLine_LocalizedStringProvider_ValidationFailed", "T_Ookii_CommandLine_CommandLineArgumentErrorCategory" ], "ValueDescriptionAttribute": "T_Ookii_CommandLine_ValueDescriptionAttribute", - "ValueTypeConverterAttribute": "T_Ookii_CommandLine_ValueTypeConverterAttribute", "VirtualTerminal": "T_Ookii_CommandLine_Terminal_VirtualTerminal", "Wrapping": "P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping", "WrappingMode.Disabled": "T_Ookii_CommandLine_WrappingMode", @@ -328,7 +275,6 @@ "M_Ookii_CommandLine_LineWrappingTextWriter_WriteAsync", "Overload_Ookii_CommandLine_LineWrappingTextWriter_WriteAsync" ], - "WriteCommand": null, "WriteCommandDescription()": "M_Ookii_CommandLine_UsageWriter_WriteCommandDescription", "WriteCommandHelpInstruction()": "M_Ookii_CommandLine_UsageWriter_WriteCommandHelpInstruction", "WriteCommandListUsageCore()": "M_Ookii_CommandLine_UsageWriter_WriteCommandListUsageCore", @@ -347,7 +293,6 @@ "M_Ookii_CommandLine_CommandLineParser_WriteUsage", "M_Ookii_CommandLine_Commands_CommandManager_WriteUsage" ], - "WriteUsageOptions": null, "WriteValueDescription()": "M_Ookii_CommandLine_UsageWriter_WriteValueDescription", "WriteValueDescriptionForDescription()": "M_Ookii_CommandLine_UsageWriter_WriteValueDescriptionForDescription" } From b3383be813c28fd7e9080526fdc54a62128e3f27 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 15 Jun 2023 15:29:18 -0700 Subject: [PATCH 147/234] Tutorial API link updates. --- docs/Tutorial.md | 103 ++++++++++++++++++++++++++--------------------- docs/refs.json | 27 ++++++++++--- 2 files changed, 77 insertions(+), 53 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 0fc9ba1a..9c29878b 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -59,7 +59,7 @@ and makes it required. > You can use the [`CommandLineArgumentAttribute`][] to specify a custom name for your argument. If you > don't, the property name is used. -The class above uses the `GeneratedParserAttribute`, which is not required, but is recommended +The class above uses the [`GeneratedParserAttribute`][], which is not required, but is recommended unless you are using an SDK older than .Net 6.0, or a language other than C# ([find out more](SourceGeneration.md)). Now replace the contents of Program.cs with the following: @@ -85,10 +85,10 @@ This code parses the arguments we defined, returns an error code if it was unsuc the contents of the file specified by the path argument to the console. The important part is the call to `Arguments.Parse()`. This static method was created by the -`GeneratedParserAttribute`, and will parse your arguments, handle and print any errors, and print +[`GeneratedParserAttribute`][], and will parse your arguments, handle and print any errors, and print usage help if required. -> If you cannot use the `GeneratedParserAttribute`, call +> If you cannot use the [`GeneratedParserAttribute`][], call > [`CommandLineParser.Parse()`][Parse()_1] instead. But wait, we didn't pass any arguments to this method? Actually, the method will call @@ -191,7 +191,7 @@ tutorial 1.0.0 By default, it shows the assembly's name and informational version. It'll also show the assembly's copyright information, if there is any (there's not in this case). You can also use the -`AssemblyTitleAttribute` or [`ApplicationFriendlyNameAttribute`][] attribute to specify a custom +[`AssemblyTitleAttribute`][] or [`ApplicationFriendlyNameAttribute`][] attribute to specify a custom name instead of the assembly name. > If you define your own argument called "Help" or "Version", the automatic arguments won't be added. @@ -309,7 +309,7 @@ Usage: tutorial [-Path] [-Help] [-Inverted] [-MaxLines ] [-Vers Run 'tutorial -Help' for more information. ``` -And because of the `ValidateRangeAttribute`, we can't specify a value less than 1 either. +And because of the [`ValidateRangeAttribute`][], we can't specify a value less than 1 either. ```bash ./tutorial ../../../tutorial.csproj -lines 0 @@ -336,7 +336,7 @@ alias for `-MaxLines`. As is `-ma`, and `-max`, etc. And `-l` is as well, as it' alias `-Lines`. > This only works if the prefix matches exactly one argument. And if you don't like this behavior, -> it can be disabled using the `ParseOptionsAttribute.AutoPrefixAliases` property. +> it can be disabled using the [`ParseOptionsAttribute.AutoPrefixAliases`][] property. Let's take a look at the usage help for our updated application, by running `./tutorial -help`: @@ -361,7 +361,7 @@ Usage: tutorial [-Path] [-Help] [-Inverted] [-MaxLines ] [-Vers Displays version information. ``` -There's a few interesting things here. The `MaxLines` property has the `ValueDescriptionAttribute` +There's a few interesting things here. The `MaxLines` property has the [`ValueDescriptionAttribute`][] applied, and we can see that the value, "Number", is used inside the angle brackets after `-MaxLines`. This is the *value description*, which is a short, typically one-word description of the type of values the argument accepts. It defaults to the type name, but "Int32" might not be very @@ -448,7 +448,7 @@ partial class Arguments } ``` -The `ParseOptionsAttribute.IsPosix` property is actually a shorthand way to set several related +The [`ParseOptionsAttribute.IsPosix`][] property is actually a shorthand way to set several related properties. The above attribute is identical to this: ```csharp @@ -596,7 +596,7 @@ command's return value from `Main()`. And that's it: we've now defined a command. However, we still need to change the application to use commands instead of just parsing arguments from a single class. To do this, we'll use the -`CommandManager` class. +[`CommandManager`][] class. First, we'll add a file named GeneratedManager.cs, with these contents: @@ -611,12 +611,12 @@ partial class GeneratedManager } ``` -The `GeneratedCommandManagerAttribute` is similar to the `GeneratedParserAttribute`, except it turns -the target class into a command manager. The `GeneratedCommandManagerAttribute` will make your class -derive from `CommandManager`, and generates code to find and instantiate the commands in this +The [`GeneratedCommandManagerAttribute`][] is similar to the [`GeneratedParserAttribute`][], except it turns +the target class into a command manager. The [`GeneratedCommandManagerAttribute`][] will make your class +derive from [`CommandManager`][], and generates code to find and instantiate the commands in this assembly. -> You can also use `CommandManager` directly, without a generated class, in which case reflection +> You can also use [`CommandManager`][] directly, without a generated class, in which case reflection > is used to find the commands. Do this if you can't use [source generation](SourceGeneration.md). Now replace the code in Program.cs with the following. @@ -641,7 +641,7 @@ command (even if currently we have only one command). The [`CommandOptions`][] c the [`ParseOptions`][] class, so it can be used to specify all the same options, in addition to some that are specific to commands. -Actually, for [`CommandOptions`][] the meaning of `IsPosix` is slightly different. It sets the same +Actually, for [`CommandOptions`][] the meaning of [`IsPosix`][IsPosix_0] is slightly different. It sets the same options as before, but also sets two additional ones. It's actually equivalent to the following: ```csharp @@ -660,7 +660,7 @@ So in addition to enabling what it did before, it also made command names case s case insensitive by default, just like argument names) and transforms their names to lowercase separated by dashes as well. -> Note that [`ParseOptions`][], and by extension [`CommandOptions`][], use a `StringComparison` +> Note that [`ParseOptions`][], and by extension [`CommandOptions`][], use a [`StringComparison`][] > value instead of just a [`CaseSensitive`][] property. The [`RunCommand()`][] method will take the arguments from [`Environment.GetCommandLineArgs()`][] @@ -696,7 +696,7 @@ description of the command. But why is the command called `read`, and not `read-command`, if it's based on the class name `ReadCommand`? If you use a name transformation for command names, it will strip the suffix -"Command" from the name by default. Use the `CommandOptions.StripCommandNameSuffix` property to +"Command" from the name by default. Use the [`CommandOptions.StripCommandNameSuffix`][] property to customize that behavior. There is a second command, `version`, which is automatically added unless there already is a command @@ -973,42 +973,51 @@ following resources: - [Class library documentation](https://www.ookii.org/Link/CommandLineDoc) - [Sample applications](../src/Samples) -[`AliasAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_AliasAttribute.htm -[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm -[`AsyncCommandBase.Run()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm -[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm -[`CaseSensitive`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive.htm -[`CommandAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_CommandAttribute.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.ShortName`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm -[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser.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 -[`CreateCommand()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm +[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AliasAttribute.htm +[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm +[`AssemblyTitleAttribute`]: https://learn.microsoft.com/dotnet/api/system.reflection.assemblytitleattribute +[`AsyncCommandBase.Run()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm +[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm +[`CaseSensitive`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive.htm +[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm +[`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm +[`CommandLineArgumentAttribute.ShortName`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm +[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandManager.RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm +[`CommandOptions.StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm +[`CommandOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandOptions.htm +[`CreateCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute [`Environment.GetCommandLineArgs()`]: https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs [`File.ReadLinesAsync()`]: https://learn.microsoft.com/dotnet/api/system.io.file.readlinesasync [`FileInfo`]: https://learn.microsoft.com/dotnet/api/system.io.fileinfo -[`GetCommand()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_CommandManager_GetCommand.htm -[`IAsyncCommand`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_IAsyncCommand.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 +[`GetCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_CommandManager_GetCommand.htm +[`IAsyncCommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_IAsyncCommand.htm [`IAsyncEnumerable`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.iasyncenumerable-1 -[`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 -[`IncludeApplicationDescriptionBeforeCommandList`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList.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 +[`IncludeApplicationDescriptionBeforeCommandList`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList.htm [`Nullable`]: https://learn.microsoft.com/dotnet/api/system.nullable-1 -[`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 -[`ParsingMode.LongShort`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParsingMode.htm -[`RunCommand()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm -[`RunCommandAsync()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.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.IsPosix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix.htm +[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm +[`ParsingMode.LongShort`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParsingMode.htm +[`RunCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm +[`RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm +[`StringComparison`]: https://learn.microsoft.com/dotnet/api/system.stringcomparison [`Take()`]: https://learn.microsoft.com/dotnet/api/system.linq.enumerable.take [`Uri`]: https://learn.microsoft.com/dotnet/api/system.uri -[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm -[Mode_2]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptionsAttribute_Mode.htm -[Parse()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[Run()_0]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm -[Run()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm -[RunAsync()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync.htm +[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm +[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm +[IsPosix_0]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_IsPosix.htm +[Mode_2]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_Mode.htm +[Parse()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[Run()_0]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm +[Run()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm +[RunAsync()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync.htm diff --git a/docs/refs.json b/docs/refs.json index 0787ae65..24c81e54 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -1,7 +1,8 @@ { - "#apiPrefix": null, - "#prefix": null, - "#suffix": null, + "#apiPrefix": "https://learn.microsoft.com/dotnet/api/", + "#prefix": "https://www.ookii.org/docs/commandline-4.0/html/", + "#suffix": ".htm", + "AddCommand": null, "AliasAttribute": "T_Ookii_CommandLine_AliasAttribute", "AllowDuplicateDictionaryKeysAttribute": "T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute", "ApplicationFriendlyNameAttribute": "T_Ookii_CommandLine_ApplicationFriendlyNameAttribute", @@ -11,9 +12,11 @@ "P_Ookii_CommandLine_Validation_DependencyValidationAttribute_Arguments", "P_Ookii_CommandLine_Validation_RequiresAnyAttribute_Arguments" ], + "Arguments.Parse()": null, "ArgumentType": "P_Ookii_CommandLine_CommandLineArgument_ArgumentType", "ArgumentValidationAttribute": "T_Ookii_CommandLine_Validation_ArgumentValidationAttribute", "ArgumentValidationWithHelpAttribute": "T_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute", + "AssemblyTitleAttribute": "#system.reflection.assemblytitleattribute", "AsyncCommandBase": "T_Ookii_CommandLine_Commands_AsyncCommandBase", "AsyncCommandBase.Run()": "M_Ookii_CommandLine_Commands_AsyncCommandBase_Run", "CancelParsing": [ @@ -101,6 +104,8 @@ "M_Ookii_CommandLine_LineWrappingTextWriter_Flush", "Overload_Ookii_CommandLine_LineWrappingTextWriter_Flush" ], + "GeneratedCommandManagerAttribute": "T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute", + "GeneratedParserAttribute": "T_Ookii_CommandLine_GeneratedParserAttribute", "GetArgument": "M_Ookii_CommandLine_CommandLineParser_GetArgument", "GetCommand()": "M_Ookii_CommandLine_Commands_CommandManager_GetCommand", "GetErrorMessage()": "M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetErrorMessage", @@ -126,6 +131,12 @@ ], "Indent": "P_Ookii_CommandLine_LineWrappingTextWriter_Indent", "Int32": "#system.int32", + "Inverted": null, + "IsPosix": [ + "P_Ookii_CommandLine_Commands_CommandOptions_IsPosix", + "P_Ookii_CommandLine_ParseOptions_IsPosix", + "P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix" + ], "IsValid()": "M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsValid", "KeyValuePair": "#system.collections.generic.keyvaluepair-2", "LineWrappingTextWriter": "T_Ookii_CommandLine_LineWrappingTextWriter", @@ -138,6 +149,8 @@ "List": "#system.collections.generic.list-1", "List": "#system.collections.generic.list-1", "LocalizedStringProvider": "T_Ookii_CommandLine_LocalizedStringProvider", + "Main()": null, + "MaxLines": null, "Mode": [ "P_Ookii_CommandLine_CommandLineParser_Mode", "P_Ookii_CommandLine_ParseOptions_Mode", @@ -186,7 +199,9 @@ "ParseOptions.UsageWriter": "P_Ookii_CommandLine_ParseOptions_UsageWriter", "ParseOptionsAttribute": "T_Ookii_CommandLine_ParseOptionsAttribute", "ParseOptionsAttribute.AllowWhiteSpaceValueSeparator": "P_Ookii_CommandLine_ParseOptionsAttribute_AllowWhiteSpaceValueSeparator", + "ParseOptionsAttribute.AutoPrefixAliases": "P_Ookii_CommandLine_ParseOptionsAttribute_AutoPrefixAliases", "ParseOptionsAttribute.CaseSensitive": "P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive", + "ParseOptionsAttribute.IsPosix": "P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix", "ParseResult.ArgumentName": "P_Ookii_CommandLine_ParseResult_ArgumentName", "ParseResult.LastException": "P_Ookii_CommandLine_ParseResult_LastException", "ParseResult.Status": "P_Ookii_CommandLine_ParseResult_Status", @@ -201,6 +216,7 @@ ], "ParsingMode.LongShort": "T_Ookii_CommandLine_ParsingMode", "ProhibitsAttribute": "T_Ookii_CommandLine_Validation_ProhibitsAttribute", + "ReadCommand": null, "RequiresAnyAttribute": "T_Ookii_CommandLine_Validation_RequiresAnyAttribute", "RequiresAttribute": "T_Ookii_CommandLine_Validation_RequiresAttribute", "ResetIndent()": "M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent", @@ -221,9 +237,7 @@ "SortedDictionary": "#system.collections.generic.sorteddictionary-2", "StreamReader": "#system.io.streamreader", "String": "#system.string", - "StringComparer.InvariantCulture": "#system.stringcomparer.invariantculture", - "StringComparer.Ordinal": "#system.stringcomparer.ordinal", - "StringComparer.OrdinalIgnoreCase": "#system.stringcomparer.ordinalignorecase", + "StringComparison": "#system.stringcomparison", "StringWriter": "#system.io.stringwriter", "StripCommandNameSuffix": "P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix", "System.ComponentModel.DescriptionAttribute": "#system.componentmodel.descriptionattribute", @@ -275,6 +289,7 @@ "M_Ookii_CommandLine_LineWrappingTextWriter_WriteAsync", "Overload_Ookii_CommandLine_LineWrappingTextWriter_WriteAsync" ], + "WriteCommand": null, "WriteCommandDescription()": "M_Ookii_CommandLine_UsageWriter_WriteCommandDescription", "WriteCommandHelpInstruction()": "M_Ookii_CommandLine_UsageWriter_WriteCommandHelpInstruction", "WriteCommandListUsageCore()": "M_Ookii_CommandLine_UsageWriter_WriteCommandListUsageCore", From dd950ce8bfc9bd808d3d937c08a31905d9f00512 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 15 Jun 2023 16:47:32 -0700 Subject: [PATCH 148/234] Minor readme update. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bce02e2..b229dcd0 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,8 @@ The .Net Standard 2.1 and .Net 6.0 and 7.0 versions utilize the framework `ReadO 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) -The .Net 7.0 version has additional support for `ISpanParsable` and `IParsable`. +The .Net 7.0 version has additional support for `required` properties, and can utilize +`ISpanParsable` and `IParsable` for argument conversion. ## Building and testing From e6dbc432d850e9432c8a9449ffa41bdf0fee0617 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 15 Jun 2023 17:27:00 -0700 Subject: [PATCH 149/234] Updated change log and migration guide. --- docs/ChangeLog.md | 43 ++++++++++++++++++++++++++++++++++++++++++- docs/Migrating.md | 23 +++++++++++++++++------ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index 7e96b9c4..de054952 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -1,6 +1,47 @@ # What’s new in Ookii.CommandLine -**IMPORTANT:** If you are upgrading from version 2.x, please check the [migration guide](Migrating.md). +## Ookii.CommandLine 4.0 + +**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. + - 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). +- 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. +- By default, only usage syntax is shown if a parsing error occurs; the help argument must be used + to get full help. +- 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. +- Various bug fixes and minor improvements. ## Ookii.CommandLine 3.1.1 diff --git a/docs/Migrating.md b/docs/Migrating.md index fa1f1598..1c887f50 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -20,15 +20,10 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ - 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`. - - This change enables more flexibility, better performance by supporting conversions using - `ReadOnlySpan`, and enables trimming your assembly when combined with - [source generation](SourceGeneration.md). - If you have existing conversions that depend on a `TypeConverter`, use the `TypeConverterArgumentConverter` as a convenient way to keep using that conversion. - Constructor parameters can no longer be used to define command line arguments. Instead, all - arguments must be defined using properties. If you were using constructor parameters to avoid - setting a default value for a non-nullable reference type, you can use the `required` keyword - instead if using .Net 7.0 or later. + arguments must be defined using properties. - 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 @@ -37,6 +32,22 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ by `ArgumentNameComparison` and `CommandNameComparison` respectively, both now taking a `StringComparison` value instead of an `IComparer`. - The `CommandInfo` type is now a class instead of a structure. +- The `ICommandWithCustomParseOptions.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. +- 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`. + +## 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. ## Breaking API changes from version 2.4 From 2d94c705974bbb9a27630ffdedd03187d7f12c83 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 15 Jun 2023 17:32:27 -0700 Subject: [PATCH 150/234] Change log API links. --- docs/ChangeLog.md | 69 +++++++++++++++++++++++++++-------------------- docs/refs.json | 4 +++ 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index de054952..c0217c26 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -6,41 +6,42 @@ 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. + - 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. + - 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` +- Converting strings to argument types is now done using Ookii.CommandLine's own [`ArgumentConverter`][] class. - - This enables conversion using `ReadOnlySpan` for better performance, makes it easier to + - 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). - 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 +- 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. + [`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. + through the [`CommandLineParser.ParseResult`][] property. - By default, only usage syntax is shown if a parsing error occurs; the help argument must be used to get full help. -- Argument validators used before conversion can implement validation on `ReadOnlySpan` for +- 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. +- The automatic version argument and command will use the [`AssemblyTitleAttribute`][] if the + [`ApplicationFriendlyNameAttribute`][] was not used. - Various bug fixes and minor improvements. ## Ookii.CommandLine 3.1.1 @@ -206,25 +207,35 @@ 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 +[`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 +[`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/refs.json b/docs/refs.json index 24c81e54..4ce37d41 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -6,6 +6,7 @@ "AliasAttribute": "T_Ookii_CommandLine_AliasAttribute", "AllowDuplicateDictionaryKeysAttribute": "T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute", "ApplicationFriendlyNameAttribute": "T_Ookii_CommandLine_ApplicationFriendlyNameAttribute", + "ArgumentConverter": "T_Ookii_CommandLine_Conversion_ArgumentConverter", "ArgumentParsed": "E_Ookii_CommandLine_CommandLineParser_ArgumentParsed", "Arguments": [ "P_Ookii_CommandLine_CommandLineParser_Arguments", @@ -65,6 +66,7 @@ "CommandOptions.AutoVersionCommand": "P_Ookii_CommandLine_Commands_CommandOptions_AutoVersionCommand", "CommandOptions.CommandFilter": "P_Ookii_CommandLine_Commands_CommandOptions_CommandFilter", "CommandOptions.CommandNameTransform": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform", + "CommandOptions.IsPosix": "P_Ookii_CommandLine_Commands_CommandOptions_IsPosix", "CommandOptions.StripCommandNameSuffix": "P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix", "Console.WindowWidth": "#system.console.windowwidth", "CreateCommand()": "Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand", @@ -194,6 +196,7 @@ "ParseOptions.AutoVersionArgument": "P_Ookii_CommandLine_ParseOptions_AutoVersionArgument", "ParseOptions.Culture": "P_Ookii_CommandLine_ParseOptions_Culture", "ParseOptions.DefaultValueDescriptions": "P_Ookii_CommandLine_ParseOptions_DefaultValueDescriptions", + "ParseOptions.IsPosix": "P_Ookii_CommandLine_ParseOptions_IsPosix", "ParseOptions.ShowUsageOnError": "P_Ookii_CommandLine_ParseOptions_ShowUsageOnError", "ParseOptions.StringProvider": "P_Ookii_CommandLine_ParseOptions_StringProvider", "ParseOptions.UsageWriter": "P_Ookii_CommandLine_ParseOptions_UsageWriter", @@ -217,6 +220,7 @@ "ParsingMode.LongShort": "T_Ookii_CommandLine_ParsingMode", "ProhibitsAttribute": "T_Ookii_CommandLine_Validation_ProhibitsAttribute", "ReadCommand": null, + "ReadOnlySpan": "#system.readonlyspan-1", "RequiresAnyAttribute": "T_Ookii_CommandLine_Validation_RequiresAnyAttribute", "RequiresAttribute": "T_Ookii_CommandLine_Validation_RequiresAttribute", "ResetIndent()": "M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent", From 619c9b7f5f3ac31b74756596d9d14d34d7a19f01 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 15 Jun 2023 17:43:18 -0700 Subject: [PATCH 151/234] Updated API links for migration guide. --- docs/Migrating.md | 108 ++++++++++++++++++++++++++++------------------ docs/refs.json | 18 ++++++++ 2 files changed, 83 insertions(+), 43 deletions(-) diff --git a/docs/Migrating.md b/docs/Migrating.md index 1c887f50..9978f914 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -15,39 +15,41 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ ## Breaking API changes from version 3.0 - The `CommandLineArgumentAttribute.ValueDescription` property has been replaced by the - `ValueDescriptionAttribute` attribute. This new attribute is not sealed, enabling derived + [`ValueDescriptionAttribute`][] attribute. This new attribute is not sealed, enabling derived attributes e.g. to load a value description from 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 - `TypeConverterArgumentConverter` as a convenient way to keep using that conversion. + [`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 + [`TypeConverterArgumentConverter`][] as a convenient way to keep using that conversion. - Constructor parameters can no longer be used to define command line arguments. Instead, all arguments must be defined using properties. -- The `CommandManager`, when using an assembly that is not the calling assembly, will only use +- 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. -- `ParseOptions.ArgumentNameComparer` and `CommandOptions.CommandNameComparer` have been replaced - by `ArgumentNameComparison` and `CommandNameComparison` respectively, both now taking a - `StringComparison` value instead of an `IComparer`. -- The `CommandInfo` type is now a class instead of a structure. -- The `ICommandWithCustomParseOptions.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. +- `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`][]. +- 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. - The `ParseOptionsAttribute.NameValueSeparator` property was replaced with - `ParseOptionsAttribute.NameValueSeparators`. + [`ParseOptionsAttribute.NameValueSeparators`][]. - The `ParseOptions.NameValueSeparator` property was replaced with - `ParseOptions.NameValueSeparators`. -- Properties that previously returned a `ReadOnlyCollection` now return an `ImmutableArray`. + [`ParseOptions.NameValueSeparators`][]. +- Properties that previously returned a [`ReadOnlyCollection`][] now return an + [`ImmutableArray`][]. ## 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. +- The default value of [`ParseOptions.ShowUsageOnError`][] has changed. ## Breaking API changes from version 2.4 @@ -108,29 +110,49 @@ 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 +[`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.ElementType`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgument_ElementType.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.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`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser_1.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 +[`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 +[`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 +[`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 +[`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter +[`TypeConverterArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter_1.htm +[`TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm +[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm +[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/refs.json b/docs/refs.json index 4ce37d41..b6653067 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -7,7 +7,13 @@ "AllowDuplicateDictionaryKeysAttribute": "T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute", "ApplicationFriendlyNameAttribute": "T_Ookii_CommandLine_ApplicationFriendlyNameAttribute", "ArgumentConverter": "T_Ookii_CommandLine_Conversion_ArgumentConverter", + "ArgumentConverterAttribute": "T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute", + "ArgumentNameComparison": [ + "P_Ookii_CommandLine_CommandLineParser_ArgumentNameComparison", + "P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison" + ], "ArgumentParsed": "E_Ookii_CommandLine_CommandLineParser_ArgumentParsed", + "ArgumentParsedEventArgs": "T_Ookii_CommandLine_ArgumentParsedEventArgs", "Arguments": [ "P_Ookii_CommandLine_CommandLineParser_Arguments", "P_Ookii_CommandLine_Validation_DependencyValidationAttribute_Arguments", @@ -20,6 +26,7 @@ "AssemblyTitleAttribute": "#system.reflection.assemblytitleattribute", "AsyncCommandBase": "T_Ookii_CommandLine_Commands_AsyncCommandBase", "AsyncCommandBase.Run()": "M_Ookii_CommandLine_Commands_AsyncCommandBase_Run", + "CancelMode": "T_Ookii_CommandLine_CancelMode", "CancelParsing": [ "P_Ookii_CommandLine_CommandLineArgument_CancelParsing", "P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing" @@ -28,6 +35,7 @@ "Category": "P_Ookii_CommandLine_CommandLineArgumentException_Category", "ClassValidationAttribute": "T_Ookii_CommandLine_Validation_ClassValidationAttribute", "CommandAttribute": "T_Ookii_CommandLine_Commands_CommandAttribute", + "CommandInfo": "T_Ookii_CommandLine_Commands_CommandInfo", "CommandLineArgument.AllowNull": "P_Ookii_CommandLine_CommandLineArgument_AllowNull", "CommandLineArgument.ElementType": "P_Ookii_CommandLine_CommandLineArgument_ElementType", "CommandLineArgumentAttribute": "T_Ookii_CommandLine_CommandLineArgumentAttribute", @@ -61,6 +69,7 @@ "CommandManager.GetCommand()": "M_Ookii_CommandLine_Commands_CommandManager_GetCommand", "CommandManager.ParseResult": "P_Ookii_CommandLine_Commands_CommandManager_ParseResult", "CommandManager.RunCommandAsync()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync", + "CommandNameComparison": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameComparison", "CommandNameTransform": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform", "CommandOptions": "T_Ookii_CommandLine_Commands_CommandOptions", "CommandOptions.AutoVersionCommand": "P_Ookii_CommandLine_Commands_CommandOptions_AutoVersionCommand", @@ -124,7 +133,9 @@ "ICommandWithCustomParsing": "T_Ookii_CommandLine_Commands_ICommandWithCustomParsing", "ICommandWithCustomParsing.Parse()": "M_Ookii_CommandLine_Commands_ICommandWithCustomParsing_Parse", "IComparable": "#system.icomparable-1", + "IComparer": "#system.collections.generic.icomparer-1", "IDictionary": "#system.collections.generic.idictionary-2", + "ImmutableArray": "#system.collections.immutable.immutablearray-1", "IncludeApplicationDescriptionBeforeCommandList": "P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList", "IncludeCommandHelpInstruction": "P_Ookii_CommandLine_UsageWriter_IncludeCommandHelpInstruction", "IncludeInUsageHelp": [ @@ -197,6 +208,7 @@ "ParseOptions.Culture": "P_Ookii_CommandLine_ParseOptions_Culture", "ParseOptions.DefaultValueDescriptions": "P_Ookii_CommandLine_ParseOptions_DefaultValueDescriptions", "ParseOptions.IsPosix": "P_Ookii_CommandLine_ParseOptions_IsPosix", + "ParseOptions.NameValueSeparators": "P_Ookii_CommandLine_ParseOptions_NameValueSeparators", "ParseOptions.ShowUsageOnError": "P_Ookii_CommandLine_ParseOptions_ShowUsageOnError", "ParseOptions.StringProvider": "P_Ookii_CommandLine_ParseOptions_StringProvider", "ParseOptions.UsageWriter": "P_Ookii_CommandLine_ParseOptions_UsageWriter", @@ -205,6 +217,7 @@ "ParseOptionsAttribute.AutoPrefixAliases": "P_Ookii_CommandLine_ParseOptionsAttribute_AutoPrefixAliases", "ParseOptionsAttribute.CaseSensitive": "P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive", "ParseOptionsAttribute.IsPosix": "P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix", + "ParseOptionsAttribute.NameValueSeparators": "P_Ookii_CommandLine_ParseOptionsAttribute_NameValueSeparators", "ParseResult.ArgumentName": "P_Ookii_CommandLine_ParseResult_ArgumentName", "ParseResult.LastException": "P_Ookii_CommandLine_ParseResult_LastException", "ParseResult.Status": "P_Ookii_CommandLine_ParseResult_Status", @@ -220,6 +233,8 @@ "ParsingMode.LongShort": "T_Ookii_CommandLine_ParsingMode", "ProhibitsAttribute": "T_Ookii_CommandLine_Validation_ProhibitsAttribute", "ReadCommand": null, + "ReadOnlyCollection": "#system.collections.objectmodel.readonlycollection-1", + "ReadOnlyMemory": "#system.readonlymemory-1", "ReadOnlySpan": "#system.readonlyspan-1", "RequiresAnyAttribute": "T_Ookii_CommandLine_Validation_RequiresAnyAttribute", "RequiresAttribute": "T_Ookii_CommandLine_Validation_RequiresAttribute", @@ -249,6 +264,9 @@ "TextFormat": "T_Ookii_CommandLine_Terminal_TextFormat", "TextWriter": "#system.io.textwriter", "ToString()": "#system.object.tostring", + "TypeConverter": "#system.componentmodel.typeconverter", + "TypeConverterArgumentConverter": "T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter_1", + "TypeConverterAttribute": "#system.componentmodel.typeconverterattribute", "Uri": "#system.uri", "UsageHelpRequest.SyntaxOnly": "T_Ookii_CommandLine_UsageHelpRequest", "UsageWriter": "T_Ookii_CommandLine_UsageWriter", From f2ffd09ec3ef5be3180bd179248b049dfaea030c Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 15 Jun 2023 18:31:35 -0700 Subject: [PATCH 152/234] Partial update to arguments documentation. --- docs/Arguments.md | 135 ++++++++++++++++++++++++-------------- docs/ChangeLog.md | 2 + docs/DefiningArguments.md | 4 ++ 3 files changed, 93 insertions(+), 48 deletions(-) diff --git a/docs/Arguments.md b/docs/Arguments.md index f4889e79..8591dfa6 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -28,25 +28,31 @@ 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. @@ -55,34 +61,56 @@ An argument can have one or more aliases: alternative names that can also be use argument. For example, an argument named `-Verbose` might use the alias `-v` as a shorter to type alternative. +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. + ## 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 `-Positional1` +argument. 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 +135,9 @@ 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`. - -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`. +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`. ## Arguments with multiple values @@ -128,15 +153,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 +187,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 +210,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,27 +223,40 @@ 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 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. +`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. + +### Using TypeConverters + +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 +`TypeConverterArgumentConverter` class to use a type's default argument converter (for example +`[ArgumentConverter(typeof(TypeConverterArgumentConverter))])`), or the +`TypeConverterArgumentConverter` class as a base class to adapt a custom `TypeConverter`. ### Enumeration type 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` diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index c0217c26..6ebce54a 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -23,6 +23,8 @@ please check the [migration guide](Migrating.md). - 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). +- 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 diff --git a/docs/DefiningArguments.md b/docs/DefiningArguments.md index 6631e40a..0a76223c 100644 --- a/docs/DefiningArguments.md +++ b/docs/DefiningArguments.md @@ -516,6 +516,10 @@ When using [long/short mode](Arguments.md#longshort-mode), the [`AliasAttribute` 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 + +TODO + ## Name transformation If your desired argument naming convention doesn't match your .Net naming convention, you can use From e1d453b5dbe239f772ed67fed4c4ba7f65480946 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 16 Jun 2023 12:50:20 -0700 Subject: [PATCH 153/234] Update arguments doc. --- docs/Arguments.md | 99 ++++++++++++++++++++------------------- docs/DefiningArguments.md | 22 +++++++++ 2 files changed, 72 insertions(+), 49 deletions(-) diff --git a/docs/Arguments.md b/docs/Arguments.md index 8591dfa6..8984c61f 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -65,6 +65,15 @@ By default, Ookii.CommandLine accepts [any prefix](DefiningArguments.md#automati 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 @@ -240,16 +249,10 @@ It is possible to override the default conversion by specifying a custom type co `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. -### Using TypeConverters - 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 -`TypeConverterArgumentConverter` class to use a type's default argument converter (for example -`[ArgumentConverter(typeof(TypeConverterArgumentConverter))])`), or the -`TypeConverterArgumentConverter` class as a base class to adapt a custom `TypeConverter`. +instead. [See here](DefiningArguments.md#using-a-typeconverter) for more information on how to +upgrade code that relied on a `TypeConverter`. ### Enumeration type conversion @@ -258,8 +261,8 @@ insensitive conversion, and allows both the names and underlying value of the en 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 @@ -285,19 +288,22 @@ the use of numeric values entirely. 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 +`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 +Ookii.CommandLine provides the `KeyValuePairConverter` class that is used by default +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 @@ -308,8 +314,8 @@ 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 @@ -320,39 +326,42 @@ nullable reference or value type (e.g. `string?` or `int?`), nothing changes. Bu 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]. +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]. 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. 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. 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. 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. +and case-sensitive argument names. For information on how to set these options, +[see here](DefiningArguments.md#longshort-mode). -For example, an argument named `--path` could have a short name `-p`. It could then be supplied -using either name: +When using long/short mode, an argument named `--path` could have a short name `-p`. It could then +be supplied using either name: ```text --path value @@ -366,7 +375,8 @@ Or: 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. +An argument can have either a short name or a long name, or both. The short name doesn't have to +use the first letter of the long name; it can be anything. 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. @@ -386,8 +396,8 @@ This is equivalent to: 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. +Besides these differences, long/short mode follows the same rules and conventions outlined above, +with all the same options. ## More information @@ -406,24 +416,15 @@ Next, let's take a look at how to [define arguments](DefiningArguments.md). [`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 [`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 [`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 diff --git a/docs/DefiningArguments.md b/docs/DefiningArguments.md index 0a76223c..64d2e0ba 100644 --- a/docs/DefiningArguments.md +++ b/docs/DefiningArguments.md @@ -257,6 +257,28 @@ The type specified must be derived from the [`TypeConverter`][] class. To make it easy to implement custom type converters to/from a string, Ookii.CommandLine provides the [`TypeConverterBase`][] type. +#### Using a TypeConverter + +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 +`TypeConverterArgumentConverter` class to use a type's default argument converter. + +```csharp +[CommandLineArgument] +[ArgumentConverter(typeof(TypeConverterArgumentConverter))] +public SomeType Argument { get; set; } +``` + +This will use `TypeDescriptor.GetTypeConverter()` 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 used. + +If you were using a custom `TypeConverter`, you can use the `TypeConverterArgumentConverter` class +as a base class to adapt it. + ### Arguments that cancel parsing You can indicate that argument parsing should stop and immediately print usage help when an argument From 8a317162e4cccc408b3a67e4e59da4f79c234609 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 16 Jun 2023 12:58:43 -0700 Subject: [PATCH 154/234] Update API links for arguments.md --- docs/Arguments.md | 57 +++++++++++++++++++++++++++++------------------ docs/refs.json | 8 +++++++ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/docs/Arguments.md b/docs/Arguments.md index 8984c61f..3329d52f 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -32,7 +32,7 @@ other platforms such as Linux or MacOS. In long/short mode, this may be the long 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.ArgumentNameComparison` +[`ParseOptionsAttribute.CaseSensitive`][] property or the [`ParseOptions.ArgumentNameComparison`][] property. The argument's value follows the name, separated by either white space (as a separate argument @@ -48,8 +48,8 @@ token), or by the argument name/value separator; by default, both a colon (`:`) 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(s) -can be customized using the `ParseOptionsAttribute.NameValueSeparators` or -`ParseOptions.NameValueSeparators` property. +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`. @@ -232,11 +232,11 @@ type. Ookii.CommandLine will try to convert the argument using the following options, in order of preference: -1. If the argument has the `ArgumentConverterAttribute` applied, the specified custom - `ArgumentConverter`. +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. + 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. @@ -245,18 +245,18 @@ This will cover the majority of types you'd want to use for arguments without ha 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. -It is possible to override the default conversion by specifying a custom type converter using the -`ArgumentConverterAttribute`. When this attribute is applied to an argument, the specified type +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. -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 +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#using-a-typeconverter) for more information on how to -upgrade code that relied on a `TypeConverter`. +upgrade code that relied on a [`TypeConverter`][]. -### Enumeration type conversion +### Enumeration conversion -The `EnumConverter` used for enumeration types relies on the `Enum.Parse()` method. It uses case +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`][]. @@ -287,23 +287,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 +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 `ArgumentConverterAttribute` as usual, but +Ookii.CommandLine provides the [`KeyValuePairConverter`][] class that is used by default +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 -`KeyConverterAttribute` and the `ValueConverterAttribute` attributes respectively. +[`KeyConverterAttribute`][] and the [`ValueConverterAttribute`][] attributes respectively. -The `KeyValuePairConverter` will use those attributes to determine which converter to +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 `ArgumentConverterAttribute` for a dictionary argument, the -`KeyConverterAttribute`, `ValueConverterAttribute`, and [`KeyValueSeparatorAttribute`][] +If you do specify the [`ArgumentConverterAttribute`][] for a dictionary argument, the +[`KeyConverterAttribute`][], [`ValueConverterAttribute`][], and [`KeyValueSeparatorAttribute`][] attributes will be ignored. ### Conversion culture @@ -326,7 +326,7 @@ nullable reference or value type (e.g. `string?` or `int?`), nothing changes. Bu 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 `ArgumentConverter` for that argument +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]. @@ -404,6 +404,8 @@ with all the same options. 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 +[`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-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 @@ -413,18 +415,29 @@ Next, let's take a look at how to [define arguments](DefiningArguments.md). [`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 +[`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-4.0/html/T_Ookii_CommandLine_Conversion_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.ArgumentNameComparison`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison.htm [`ParseOptions.Culture`]: https://www.ookii.org/docs/commandline-3.1/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-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.NameValueSeparators`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_NameValueSeparators.htm [`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm [`String`]: https://learn.microsoft.com/dotnet/api/system.string +[`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter [`Uri`]: https://learn.microsoft.com/dotnet/api/system.uri +[`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm [NullArgumentValue_0]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm diff --git a/docs/refs.json b/docs/refs.json index b6653067..2e0915ec 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -104,6 +104,8 @@ "P_Ookii_CommandLine_LineWrappingTextWriter_Encoding", "#system.text.encoding" ], + "Enum.Parse()": "#system.enum.parse", + "EnumConverter": "T_Ookii_CommandLine_Conversion_EnumConverter", "Environment.GetCommandLineArgs()": "#system.environment.getcommandlineargs", "Error": "P_Ookii_CommandLine_ParseOptions_Error", "ErrorCategory": "P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_ErrorCategory", @@ -145,13 +147,17 @@ "Indent": "P_Ookii_CommandLine_LineWrappingTextWriter_Indent", "Int32": "#system.int32", "Inverted": null, + "IParsable": "#system.iparsable-1", + "ISpanParsable": "#system.ispanparsable-1", "IsPosix": [ "P_Ookii_CommandLine_Commands_CommandOptions_IsPosix", "P_Ookii_CommandLine_ParseOptions_IsPosix", "P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix" ], "IsValid()": "M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsValid", + "KeyConverterAttribute": "T_Ookii_CommandLine_Conversion_KeyConverterAttribute", "KeyValuePair": "#system.collections.generic.keyvaluepair-2", + "KeyValuePairConverter": "T_Ookii_CommandLine_Conversion_KeyValuePairConverter_2", "LineWrappingTextWriter": "T_Ookii_CommandLine_LineWrappingTextWriter", "LineWrappingTextWriter.ForConsoleError()": "M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleError", "LineWrappingTextWriter.ForConsoleOut()": "M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleOut", @@ -203,6 +209,7 @@ ], "ParseOptions": "T_Ookii_CommandLine_ParseOptions", "ParseOptions.AllowWhiteSpaceValueSeparator": "P_Ookii_CommandLine_ParseOptions_AllowWhiteSpaceValueSeparator", + "ParseOptions.ArgumentNameComparison": "P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison", "ParseOptions.ArgumentNameTransform": "P_Ookii_CommandLine_ParseOptions_ArgumentNameTransform", "ParseOptions.AutoVersionArgument": "P_Ookii_CommandLine_ParseOptions_AutoVersionArgument", "ParseOptions.Culture": "P_Ookii_CommandLine_ParseOptions_Culture", @@ -291,6 +298,7 @@ "M_Ookii_CommandLine_LocalizedStringProvider_ValidationFailed", "T_Ookii_CommandLine_CommandLineArgumentErrorCategory" ], + "ValueConverterAttribute": "T_Ookii_CommandLine_Conversion_ValueConverterAttribute", "ValueDescriptionAttribute": "T_Ookii_CommandLine_ValueDescriptionAttribute", "VirtualTerminal": "T_Ookii_CommandLine_Terminal_VirtualTerminal", "Wrapping": "P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping", From 5e780ef451205729eedc3a4a4f6d5cedd2e280ba Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 16 Jun 2023 13:06:01 -0700 Subject: [PATCH 155/234] Mention moved converter types in migration guide. --- docs/Arguments.md | 26 +++++++++++++------------- docs/Migrating.md | 10 ++++++++++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/Arguments.md b/docs/Arguments.md index 3329d52f..a480915b 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -403,12 +403,12 @@ with all the same options. 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 +[`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-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 +[`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 @@ -425,19 +425,19 @@ Next, let's take a look at how to [define arguments](DefiningArguments.md). [`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-4.0/html/T_Ookii_CommandLine_Conversion_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 +[`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-3.1/html/P_Ookii_CommandLine_ParseOptions_Culture.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-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 +[`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-3.1/html/T_Ookii_CommandLine_ParseOptionsAttribute.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 [`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter [`Uri`]: https://learn.microsoft.com/dotnet/api/system.uri [`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm -[NullArgumentValue_0]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm +[NullArgumentValue_0]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm diff --git a/docs/Migrating.md b/docs/Migrating.md index 9978f914..0714d28a 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -23,6 +23,12 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`TypeConverterAttribute`][]. - If you have existing conversions that depend on a [`TypeConverter`][], use the [`TypeConverterArgumentConverter`][] 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. - The [`CommandManager`][], when using an assembly that is not the calling assembly, will only use @@ -137,6 +143,9 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`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-4.0/html/N_Ookii_CommandLine_Commands.htm @@ -152,6 +161,7 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute [`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm [`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm +[`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.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 From d3cc191c3fb170e4c6afefd531468ed10f878569 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 16 Jun 2023 14:16:38 -0700 Subject: [PATCH 156/234] Updates to defining arguments doc. --- docs/Arguments.md | 2 +- docs/DefiningArguments.md | 496 ++++++++++++++++++-------------------- 2 files changed, 233 insertions(+), 265 deletions(-) diff --git a/docs/Arguments.md b/docs/Arguments.md index a480915b..800511d2 100644 --- a/docs/Arguments.md +++ b/docs/Arguments.md @@ -251,7 +251,7 @@ 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#using-a-typeconverter) for more information on how to +instead. [See here](DefiningArguments.md#custom-type-conversion) for more information on how to upgrade code that relied on a [`TypeConverter`][]. ### Enumeration conversion diff --git a/docs/DefiningArguments.md b/docs/DefiningArguments.md index 64d2e0ba..79b541b0 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 +{ +} +``` + +This enables the use of [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,18 +61,38 @@ public string? SomeArgument { get; set; } This creates an argument named `-OtherName`. -### Required and positional arguments +### Positional arguments -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. +There are two ways to make an argument positional. + +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` 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. @@ -88,9 +124,26 @@ public int Argument2 { get; set; } public int Argument1 { 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; } +``` + +Now, 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 @@ -127,25 +180,35 @@ 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. +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. This limitation -does not apply if [source generation](SourceGeneration.md) is used. +If you are _not_ using source generation, 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 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, @@ -157,16 +220,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 when not using source generation, you should use either +[`Dictionary`][] or [`IDictionary`][] as the type of the property, +otherwise the nullability of the value type 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, 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 @@ -174,37 +255,20 @@ 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 be 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. ### Argument descriptions @@ -228,42 +292,38 @@ 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 make it easy to implement custom type converters to/from a string, Ookii.CommandLine provides -the [`TypeConverterBase`][] type. - -#### Using a TypeConverter - -Previous versions of Ookii.CommandLine used .Net's `TypeConverter` class. Starting with +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 +To help with transitioning code that relied on [`TypeConverter`][], you can use the `TypeConverterArgumentConverter` class to use a type's default argument converter. ```csharp @@ -272,41 +332,59 @@ To help with transitioning code that relied on `TypeConverter`, you can use the public SomeType Argument { get; set; } ``` -This will use `TypeDescriptor.GetTypeConverter()` function to get the default `TypeConverter` for +This will use `TypeDescriptor.GetTypeConverter()` 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 used. +the main reason [`TypeConverter`][] is no longer used. -If you were using a custom `TypeConverter`, you can use the `TypeConverterArgumentConverter` class +If you were using a custom [`TypeConverter`][], you can use the `TypeConverterArgumentConverter` class as a base class to adapt it. ### 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 return 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. ```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` 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);` @@ -322,186 +400,83 @@ 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; -} -``` - -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) - { - /* ... */ - } + return CancelMode.Abort; } ``` -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. +When using a signature that returns `bool`, returning `true` is equivalent to `CancelMode.None` and +`false` is equivalent to `CancelMode.Abort`. -```csharp -public class MyArguments -{ - public MyArguments(string arg1, int arg2, float arg3 = 0f) - { - /* ... */ - } +Using a signature that returns `void` is equivalent to returning `CancelMode.None`. - [CommandLineArgument(Position = 0)] - public int PropertyArg { get; set; } -} -``` +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. -In this case, `-PropertyArg` will be the fourth positional argument. +## Applying parse options -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). +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. -The [`ValueDescriptionAttribute`][] is used to set a custom value description, and full descriptions -are still set using the [`DescriptionAttribute`][]. +For example, the following disables the use of the `/` argument prefix on Windows, and always uses +only `-`. ```csharp -public MyArguments( - [ArgumentName("Count", IsShort = true)] - [ValueDescription("Number")], - [Description("Provides a count to the application.")] - int count) +[GeneratedParser] +[ParseOptions(ArgumentNamesPrefixes = new[] { '-' })] +partial class Arguments { - /* ... */ -} -``` - -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. - -### CommandLineParser injection - -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. - -Using [`CommandLineParser`][] injection can be used by itself, or combined with other parameters that -define arguments. - -```csharp -public MyArguments(CommandLineParser parser, string argument) -{ -} -``` - -## 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` 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;} @@ -511,7 +486,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 @@ -520,8 +504,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`: @@ -534,13 +517,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 -TODO +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 @@ -558,12 +556,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; @@ -576,24 +575,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. @@ -626,10 +607,6 @@ disable either automatic argument using the [`ParseOptions`][]. 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 @@ -637,9 +614,7 @@ Next, we'll take a look at how to [parse the arguments we've defined](ParsingArg [`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 [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute @@ -647,22 +622,15 @@ Next, we'll take a look at how to [parse the arguments we've defined](ParsingArg [`ICollection`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.icollection-1 [`IDictionary`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.idictionary-2 [`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 [`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 From 3b7d838768a9bed4f1bd257ce32a2af0746d89e4 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 16 Jun 2023 14:58:49 -0700 Subject: [PATCH 157/234] Updated API docs for DefiningArguments.md --- docs/DefiningArguments.md | 112 ++++++++++++++++++++++---------------- docs/refs.json | 17 ++++++ 2 files changed, 82 insertions(+), 47 deletions(-) diff --git a/docs/DefiningArguments.md b/docs/DefiningArguments.md index 79b541b0..3c3b9974 100644 --- a/docs/DefiningArguments.md +++ b/docs/DefiningArguments.md @@ -65,7 +65,7 @@ This creates an argument named `-OtherName`. There are two ways to make an argument positional. -When using [source generation](SourceGeneration.md), you can use the `CommandLineArgumentAttribute.IsPositional` +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 @@ -83,7 +83,7 @@ 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` property +> [`IsPositional`][] property is only supported when using source generation. The [`Position`][Position_1] property > works with both source generation and reflection. ```csharp @@ -295,7 +295,7 @@ to the name of the argument type (in the case of a multi-value argument, the ele 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 `ValueDescriptionAttribute` attribute. +To specify a custom value description, use the [`ValueDescriptionAttribute`][] attribute. ```csharp [CommandLineArgument] @@ -309,7 +309,7 @@ This should _not_ be used for the description of the argument's purpose; use the ### Custom type conversion If you want to use a non-default conversion from string, you can specify a custom type converter -using the `ArgumentConverterAttribute`. +using the [`ArgumentConverterAttribute`][]. ```csharp [CommandLineArgument] @@ -317,14 +317,14 @@ using the `ArgumentConverterAttribute`. public int Argument { get; set; } ``` -The type specified must be derived from the `ArgumentConverter` class. +The type specified must be derived from the [`ArgumentConverter`][] class. 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 +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 -`TypeConverterArgumentConverter` class to use a type's default argument converter. +[`TypeConverterArgumentConverter`][] class to use a type's default argument converter. ```csharp [CommandLineArgument] @@ -332,11 +332,11 @@ To help with transitioning code that relied on [`TypeConverter`][], you can use public SomeType Argument { get; set; } ``` -This will use `TypeDescriptor.GetTypeConverter()` function to get the default [`TypeConverter`][] for +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 used. +the main reason [`TypeConverter`][] is no longer the default for converting arguments. -If you were using a custom [`TypeConverter`][], you can use the `TypeConverterArgumentConverter` class +If you were using a custom [`TypeConverter`][], you can use the [`TypeConverterArgumentConverter`][] class as a base class to adapt it. ### Arguments that cancel parsing @@ -344,9 +344,9 @@ as a base class to adapt it. You can indicate that argument parsing should stop immediately return when an argument is supplied by setting the [`CommandLineArgumentAttribute.CancelParsing`][] property. -When this property is set to `CancelMode.Abort`, parsing is stopped when the argument is +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 +[`CommandLineParser.Parse()`][] will return null. The [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] and the static [`Parse()`][Parse()_1] helper methods will automatically print usage in this case. @@ -360,16 +360,16 @@ 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` property to `CancelMode.Success`, parsing is stopped, and the rest +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 +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 +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 +[`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). @@ -404,12 +404,12 @@ is supplied, even if its value is explicitly set to false (if you want to distin 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, unless you provide an `ArgumentConverter` that can convert +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 `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 +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. @@ -423,10 +423,10 @@ public static CancelMode MoreHelp(CommandLineParser parser) } ``` -When using a signature that returns `bool`, returning `true` is equivalent to `CancelMode.None` and -`false` is equivalent to `CancelMode.Abort`. +When using a signature that returns `bool`, returning `true` is equivalent to [`CancelMode.None`][] and +`false` is equivalent to [`CancelMode.Abort`][]. -Using a signature that returns `void` is equivalent to returning `CancelMode.None`. +Using a signature that returns `void` is equivalent to returning [`CancelMode.None`][]. 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 @@ -457,7 +457,7 @@ if you want to mimic typical POSIX conventions: the mode itself, case sensitive and dash-case [name transformation](#name-transformation). This can be done with either the [`ParseOptionsAttribute`][] attribute or the [`ParseOptions`][] class. -A convenient `IsPosix` property is provided on either class, that sets all relevant options when +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 @@ -537,8 +537,8 @@ 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. +If you do not want to use automatic prefix aliases, set the [`ParseOptionsAttribute.AutoPrefixAliases`][] +or [`ParseOptions.AutoPrefixAliases`][] property to false. ## Name transformation @@ -606,31 +606,49 @@ disable either automatic argument using the [`ParseOptions`][]. 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 -[`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`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.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 [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute [`Dictionary`]: https://learn.microsoft.com/dotnet/api/system.collections.generic.dictionary-2 [`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 -[`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 +[`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 +[`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 -[CancelParsing_1]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.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 +[`TypeConverterArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter.htm +[`TypeConverterArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter_1.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 +[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/refs.json b/docs/refs.json index 2e0915ec..760c42d8 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -26,7 +26,11 @@ "AssemblyTitleAttribute": "#system.reflection.assemblytitleattribute", "AsyncCommandBase": "T_Ookii_CommandLine_Commands_AsyncCommandBase", "AsyncCommandBase.Run()": "M_Ookii_CommandLine_Commands_AsyncCommandBase_Run", + "Bar": null, "CancelMode": "T_Ookii_CommandLine_CancelMode", + "CancelMode.Abort": "T_Ookii_CommandLine_CancelMode", + "CancelMode.None": "T_Ookii_CommandLine_CancelMode", + "CancelMode.Success": "T_Ookii_CommandLine_CancelMode", "CancelParsing": [ "P_Ookii_CommandLine_CommandLineArgument_CancelParsing", "P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing" @@ -43,6 +47,7 @@ "CommandLineArgumentAttribute.DefaultValue": "P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue", "CommandLineArgumentAttribute.IsHidden": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden", "CommandLineArgumentAttribute.IsLong": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong", + "CommandLineArgumentAttribute.IsPositional": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional", "CommandLineArgumentAttribute.IsRequired": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired", "CommandLineArgumentAttribute.IsShort": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort", "CommandLineArgumentAttribute.Position": "P_Ookii_CommandLine_CommandLineArgumentAttribute_Position", @@ -111,12 +116,14 @@ "ErrorCategory": "P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_ErrorCategory", "File.ReadLinesAsync()": "#system.io.file.readlinesasync", "FileInfo": "#system.io.fileinfo", + "FileName": null, "FlagsAttribute": "#system.flagsattribute", "Flush()": [ "M_Ookii_CommandLine_LineWrappingTextWriter_Flush_1", "M_Ookii_CommandLine_LineWrappingTextWriter_Flush", "Overload_Ookii_CommandLine_LineWrappingTextWriter_Flush" ], + "Foo": null, "GeneratedCommandManagerAttribute": "T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute", "GeneratedParserAttribute": "T_Ookii_CommandLine_GeneratedParserAttribute", "GetArgument": "M_Ookii_CommandLine_CommandLineParser_GetArgument", @@ -149,6 +156,7 @@ "Inverted": null, "IParsable": "#system.iparsable-1", "ISpanParsable": "#system.ispanparsable-1", + "IsPositional": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional", "IsPosix": [ "P_Ookii_CommandLine_Commands_CommandOptions_IsPosix", "P_Ookii_CommandLine_ParseOptions_IsPosix", @@ -211,6 +219,7 @@ "ParseOptions.AllowWhiteSpaceValueSeparator": "P_Ookii_CommandLine_ParseOptions_AllowWhiteSpaceValueSeparator", "ParseOptions.ArgumentNameComparison": "P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison", "ParseOptions.ArgumentNameTransform": "P_Ookii_CommandLine_ParseOptions_ArgumentNameTransform", + "ParseOptions.AutoPrefixAliases": "P_Ookii_CommandLine_ParseOptions_AutoPrefixAliases", "ParseOptions.AutoVersionArgument": "P_Ookii_CommandLine_ParseOptions_AutoVersionArgument", "ParseOptions.Culture": "P_Ookii_CommandLine_ParseOptions_Culture", "ParseOptions.DefaultValueDescriptions": "P_Ookii_CommandLine_ParseOptions_DefaultValueDescriptions", @@ -227,6 +236,7 @@ "ParseOptionsAttribute.NameValueSeparators": "P_Ookii_CommandLine_ParseOptionsAttribute_NameValueSeparators", "ParseResult.ArgumentName": "P_Ookii_CommandLine_ParseResult_ArgumentName", "ParseResult.LastException": "P_Ookii_CommandLine_ParseResult_LastException", + "ParseResult.RemainingArguments": "P_Ookii_CommandLine_ParseResult_RemainingArguments", "ParseResult.Status": "P_Ookii_CommandLine_ParseResult_Status", "ParseStatus.Error": "T_Ookii_CommandLine_ParseStatus", "ParseWithErrorHandling()": [ @@ -238,6 +248,10 @@ "Overload_Ookii_CommandLine_CommandLineParser_ParseWithErrorHandling" ], "ParsingMode.LongShort": "T_Ookii_CommandLine_ParsingMode", + "Position": [ + "P_Ookii_CommandLine_CommandLineArgument_Position", + "P_Ookii_CommandLine_CommandLineArgumentAttribute_Position" + ], "ProhibitsAttribute": "T_Ookii_CommandLine_Validation_ProhibitsAttribute", "ReadCommand": null, "ReadOnlyCollection": "#system.collections.objectmodel.readonlycollection-1", @@ -260,6 +274,7 @@ "RunCommandAsync()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync", "SetConsoleMode": "https://learn.microsoft.com/windows/console/setconsolemode", "ShortAliasAttribute": "T_Ookii_CommandLine_ShortAliasAttribute", + "SomeName": null, "SortedDictionary": "#system.collections.generic.sorteddictionary-2", "StreamReader": "#system.io.streamreader", "String": "#system.string", @@ -272,8 +287,10 @@ "TextWriter": "#system.io.textwriter", "ToString()": "#system.object.tostring", "TypeConverter": "#system.componentmodel.typeconverter", + "TypeConverterArgumentConverter": "T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter", "TypeConverterArgumentConverter": "T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter_1", "TypeConverterAttribute": "#system.componentmodel.typeconverterattribute", + "TypeDescriptor.GetConverter()": "#system.componentmodel.typedescriptor.getconverter", "Uri": "#system.uri", "UsageHelpRequest.SyntaxOnly": "T_Ookii_CommandLine_UsageHelpRequest", "UsageWriter": "T_Ookii_CommandLine_UsageWriter", From 46130b5833a880648c51f2c97fba74b672ecb768 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 16 Jun 2023 15:17:50 -0700 Subject: [PATCH 158/234] Parsing arguments updates. --- docs/ParsingArguments.md | 92 +++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/docs/ParsingArguments.md b/docs/ParsingArguments.md index b6c41778..ce5d6d21 100644 --- a/docs/ParsingArguments.md +++ b/docs/ParsingArguments.md @@ -3,18 +3,19 @@ When you have [defined the command line arguments](DefiningArguments.md), you can parse the command line to determine their values. There are two basic ways to do this, described below. -## Using the static helper method +## Using the static helper methods -The easiest way to parse the arguments is using the static [`CommandLineParser.Parse()`][] helper -methods. These methods take care of parsing the arguments, handling errors, and printing usage help -if necessary. +The easiest way to parse the arguments is using the static `Parse()` methods that is generated for +your arguments class when using [source generation](SourceGeneration.md) with the +`GeneratedParserAttribute`. These methods take care of parsing the arguments, handling errors, and +printing usage help if necessary. A basic usage sample for the [`CommandLineParser`][] class is as follows: ```csharp public static int Main() { - var arguments = CommandLineParser.Parse(); + var arguments = MyArguments.Parse(); if (arguments == null) { return 1; // Or a suitable error code. @@ -27,11 +28,13 @@ public static int Main() This overload takes the arguments from the [`Environment.GetCommandLineArgs()`][] method, so there is no need to pass them manually (though you can if desired), and the default [`ParseOptions`][]. -If argument parsing is successful, the [`CommandLineParser`][] will create a new instance of the class -defining the arguments, passing the values parsed from the command line to the constructor -parameters (if any). It will then set the value of each property to the value of the corresponding -argument. This is not done in any particular order, so do not write code that makes assumptions -about this. Finally, it will return the instance. +If you cannot use source generation, you can call one of the [`CommandLineParser.Parse()`][] +methods, which work the same way as the generated method. + +If argument parsing is successful, the [`CommandLineParser`][] will create a new instance of the +class defining the arguments. It will then set the value of each property to the value of the +corresponding argument. This is not done in any particular order, so do not write code that makes +assumptions about this. Finally, it will return the instance. Argument parsing can fail for a number of reason, including: @@ -43,24 +46,25 @@ Argument parsing can fail for a number of reason, including: - Argument value conversion failed for one of the arguments. - An argument failed [validation](Validation.md). -See the [`CommandLineArgumentErrorCategory`][] enumeration for more information. In addition, parsing -could have been canceled by an argument using the [`CommandLineArgumentAttribute.CancelParsing`][] -property, a method argument, or the automatic `-Help` and `-Version` arguments. +See the [`CommandLineArgumentErrorCategory`][] enumeration for more information. In addition, +parsing could have been canceled by an argument using the +[`CommandLineArgumentAttribute.CancelParsing`][] property with `CancelMode.Abort`, a method +argument, or the automatic `-Help` and `-Version` arguments. -If argument parsing does fail or was canceled, the static [`Parse()`][Parse()_1] method -returns null. The method has already printed error and usage information, and there's nothing you -need to do except exit your application. +If argument parsing does fail or was canceled, the generated `Parse()` method (as well as the static +[`CommandLineParser.Parse()`][] method) returns null. The method has already printed error and +usage information, and there's nothing you need to do except exit your application. -The static [`Parse()`][Parse()_1] will not throw an exception, unless the arguments type +The generated `Parse()` methods and the static [`Parse()`][Parse()_1] method will never throw +a `CommandLineArgumentArgumentException`. They can throw other exceptions if the arguments type violates one of the rules for valid arguments (such as defining an optional positional argument after a required one). An exception from this method typically indicates a mistake in your arguments -class. +class. When using source generation, these kinds of errors are typically caught at compile time. You can customize various aspects of the parsing behavior using either the [`ParseOptionsAttribute`][], applied to your arguments class, or a [`ParseOptions`][] instance -passed to the [`Parse()`][Parse()_1] method. The latter can be used to set a few options not -available with the [`ParseOptionsAttribute`][], including options to customize the usage help and -error messages. +passed to the `Parse()` method. The latter can be used to set a few options not available with the +[`ParseOptionsAttribute`][], including options to customize the usage help and error messages. The [`ParseOptions`][] class can even be used to redirect where errors and help are written. @@ -69,7 +73,7 @@ using var writer = LineWrappingTextWriter.ForStringWriter(); var options = new ParseOptions() { Error = writer, - Mode = ParsingMode.LongShort, + IsPosix = true, DuplicateArguments = ErrorMode.Warning, UsageWriter = new UsageWriter(writer); }; @@ -83,11 +87,6 @@ if (arguments == null) } ``` -In the vast majority of cases, [`ParseOptionsAttribute`][] and [`ParseOptions`][] should be sufficient to -customize the parsing behavior to your liking. If you need access to the [`CommandLineParser`][] instance -after parsing finished, you can use [injection](DefiningArguments.md#commandlineparser-injection), -so it should rarely be necessary to use the manual parsing method. - ### Custom error messages If you wish to customize the error messages shown to the user if parsing fails, for example to @@ -96,7 +95,8 @@ the source for all error messages, as well as a number of other strings used by Create a class that derives from the [`LocalizedStringProvider`][] class and override its members to customize any strings you wish to change. You can specify a custom string provider using the -[`ParseOptions.StringProvider`][] class. +[`ParseOptions.StringProvider`][] class. Localizing some strings used in the usage help may also +require you to create a custom `UsageWriter`. Alternatively, if you need more error information, you can use the manual parsing method below, and use the [`CommandLineArgumentException.Category`][] property to determine the cause of the exception @@ -114,18 +114,23 @@ In this case, you can manually create an instance of the [`CommandLineParser` the instance [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] or [`Parse()`][Parse()_5] method. > The [`CommandLineParser`][] class is a helper class that derives from [`CommandLineParser`][] -> and provides strongly-typed [`Parse()`][Parse()_5] and [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] methods. +> and provides strongly-typed [`Parse()`][Parse()_5] and +> [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] methods. + +If you are using source generation, you can call the generated `CreateParser()` method that is added +to your class to get a `CommandLineParser` instance. Otherwise, simply use +`new CommandLineParser()`. -Using [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] is the easiest in this case, because it will still handle -printing error messages and usage help, the same as the static [`Parse()`][Parse()_1] method. If you want -more information about the error that occurred, you can access the [`CommandLineParser.ParseResult`][] -property after parsing. +Using [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] is the easiest in this case, because +it will still handle printing error messages and usage help, the same as the generated `Parse()` +method and static [`Parse()`][Parse()_1] methods. If you want more information about the error +that occurred, you can access the [`CommandLineParser.ParseResult`][] property after parsing. For example, you can use this approach if you want to return a success status when parsing was canceled, but not when a parsing error occurred: ```csharp -var parser = new CommandLineParser(); +var parser = MyArguments.CreateParser(); var arguments = parser.ParseWithErrorHandling(); if (arguments == null) { @@ -133,9 +138,15 @@ if (arguments == null) } ``` +The status will be set to `ParseStatus.Canceled` if parsing was canceled with `CancelMode.Abort`. + You can also use the [`ParseResult.ArgumentName`][] property to determine which argument canceled -parsing in this case. If an error occurred, the status will be [`ParseStatus.Error`][] and you can use -the [`ParseResult.LastException`][] property to access the actual error that occurred. +parsing in this case. If an error occurred, the status will be [`ParseStatus.Error`][] and you can +use the [`ParseResult.LastException`][] property to access the actual error that occurred. + +If parsing was canceled using `CancelMode.Success`, the status will be `ParseStatus.Success`, but +`ParseResult.ArgumentName` will be non-null and set to the argument that canceled parsing. Use the +`ParseResult.RemainingArguments` property to get any arguments that were not parsed. For the most fine grained control, you can use the [`CommandLineParser.Parse()`][] method, which lets you handle errors manually. @@ -165,7 +176,7 @@ Here is a basic sample of manual parsing and error handling using the [`Parse()` ```csharp static int Main() { - var parser = new CommandLineParser(); + var parser = MyArguments.CreateParser(); try { var arguments = parser.Parse(); @@ -190,9 +201,10 @@ static int Main() If you wish to customize the behavior, that can still be done using the [`ParseOptionsAttribute`][] attribute and the [`ParseOptions`][] class (which you can pass to the [`CommandLineParser`][] -constructor). Some properties of the [`ParseOptions`][] class (like [`Error`][]) are not used with -the [`Parse()`][Parse()_5] methods, as they apply to the [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] and the static -[`Parse()`][Parse()_1] methods only. +constructor or the generated `CreateParser()` method). Some properties of the [`ParseOptions`][] +class (like [`Error`][]) are not used with the [`Parse()`][Parse()_5] methods, as they apply to the +[`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] and the static [`Parse()`][Parse()_1] +methods only. Next, we'll take a look at [generating usage help](UsageHelp.md). From 9ee481d59356d3272e81c222735a8f0ed8836c8b Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 16 Jun 2023 15:28:19 -0700 Subject: [PATCH 159/234] Updated API refs for ParsingArguments.md. --- docs/ParsingArguments.md | 101 ++++++++++++----------- docs/refs.json | 9 +- src/Ookii.CommandLine/IParser.cs | 5 ++ src/Ookii.CommandLine/IParserProvider.cs | 5 ++ 4 files changed, 73 insertions(+), 47 deletions(-) diff --git a/docs/ParsingArguments.md b/docs/ParsingArguments.md index ce5d6d21..a93003e6 100644 --- a/docs/ParsingArguments.md +++ b/docs/ParsingArguments.md @@ -5,10 +5,10 @@ line to determine their values. There are two basic ways to do this, described b ## Using the static helper methods -The easiest way to parse the arguments is using the static `Parse()` methods that is generated for -your arguments class when using [source generation](SourceGeneration.md) with the -`GeneratedParserAttribute`. These methods take care of parsing the arguments, handling errors, and -printing usage help if necessary. +The easiest way to parse the arguments is using the static [`Parse()`][Parse()_7] methods that are +generated for your arguments class when using [source generation](SourceGeneration.md) with the +[`GeneratedParserAttribute`][]. These methods take care of parsing the arguments, handling errors, +and printing usage help if necessary. A basic usage sample for the [`CommandLineParser`][] class is as follows: @@ -48,22 +48,22 @@ Argument parsing can fail for a number of reason, including: See the [`CommandLineArgumentErrorCategory`][] enumeration for more information. In addition, parsing could have been canceled by an argument using the -[`CommandLineArgumentAttribute.CancelParsing`][] property with `CancelMode.Abort`, a method +[`CommandLineArgumentAttribute.CancelParsing`][] property with [`CancelMode.Abort`][], a method argument, or the automatic `-Help` and `-Version` arguments. -If argument parsing does fail or was canceled, the generated `Parse()` method (as well as the static +If argument parsing does fail or was canceled, the generated [`Parse()`][Parse()_7] method (as well as the static [`CommandLineParser.Parse()`][] method) returns null. The method has already printed error and usage information, and there's nothing you need to do except exit your application. -The generated `Parse()` methods and the static [`Parse()`][Parse()_1] method will never throw -a `CommandLineArgumentArgumentException`. They can throw other exceptions if the arguments type -violates one of the rules for valid arguments (such as defining an optional positional argument -after a required one). An exception from this method typically indicates a mistake in your arguments -class. When using source generation, these kinds of errors are typically caught at compile time. +The generated [`Parse()`][Parse()_7] methods and the static [`Parse()`][Parse()_1] method will never throw +a [`CommandLineArgumentException`][]. They can throw other exceptions if the arguments type violates one +of the rules for valid arguments (such as defining an optional positional argument after a required +one). An exception from this method typically indicates a mistake in your arguments class. When +using source generation, these kinds of errors are typically caught at compile time. You can customize various aspects of the parsing behavior using either the [`ParseOptionsAttribute`][], applied to your arguments class, or a [`ParseOptions`][] instance -passed to the `Parse()` method. The latter can be used to set a few options not available with the +passed to the [`Parse()`][Parse()_7] method. The latter can be used to set a few options not available with the [`ParseOptionsAttribute`][], including options to customize the usage help and error messages. The [`ParseOptions`][] class can even be used to redirect where errors and help are written. @@ -96,7 +96,7 @@ the source for all error messages, as well as a number of other strings used by Create a class that derives from the [`LocalizedStringProvider`][] class and override its members to customize any strings you wish to change. You can specify a custom string provider using the [`ParseOptions.StringProvider`][] class. Localizing some strings used in the usage help may also -require you to create a custom `UsageWriter`. +require you to create a custom [`UsageWriter`][]. Alternatively, if you need more error information, you can use the manual parsing method below, and use the [`CommandLineArgumentException.Category`][] property to determine the cause of the exception @@ -117,12 +117,12 @@ the instance [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] or [`Parse > and provides strongly-typed [`Parse()`][Parse()_5] and > [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] methods. -If you are using source generation, you can call the generated `CreateParser()` method that is added -to your class to get a `CommandLineParser` instance. Otherwise, simply use +If you are using source generation, you can call the generated [`CreateParser()`][CreateParser()_1] method that is added +to your class to get a [`CommandLineParser`][] instance. Otherwise, simply use `new CommandLineParser()`. Using [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] is the easiest in this case, because -it will still handle printing error messages and usage help, the same as the generated `Parse()` +it will still handle printing error messages and usage help, the same as the generated [`Parse()`][Parse()_7] method and static [`Parse()`][Parse()_1] methods. If you want more information about the error that occurred, you can access the [`CommandLineParser.ParseResult`][] property after parsing. @@ -138,15 +138,15 @@ if (arguments == null) } ``` -The status will be set to `ParseStatus.Canceled` if parsing was canceled with `CancelMode.Abort`. +The status will be set to [`ParseStatus.Canceled`][] if parsing was canceled with [`CancelMode.Abort`][]. You can also use the [`ParseResult.ArgumentName`][] property to determine which argument canceled parsing in this case. If an error occurred, the status will be [`ParseStatus.Error`][] and you can use the [`ParseResult.LastException`][] property to access the actual error that occurred. -If parsing was canceled using `CancelMode.Success`, the status will be `ParseStatus.Success`, but -`ParseResult.ArgumentName` will be non-null and set to the argument that canceled parsing. Use the -`ParseResult.RemainingArguments` property to get any arguments that were not parsed. +If parsing was canceled using [`CancelMode.Success`][], the status will be [`ParseStatus.Success`][], but +[`ParseResult.ArgumentName`][] will be non-null and set to the argument that canceled parsing. Use the +[`ParseResult.RemainingArguments`][] property to get any arguments that were not parsed. For the most fine grained control, you can use the [`CommandLineParser.Parse()`][] method, which lets you handle errors manually. @@ -201,36 +201,45 @@ static int Main() If you wish to customize the behavior, that can still be done using the [`ParseOptionsAttribute`][] attribute and the [`ParseOptions`][] class (which you can pass to the [`CommandLineParser`][] -constructor or the generated `CreateParser()` method). Some properties of the [`ParseOptions`][] +constructor or the generated [`CreateParser()`][CreateParser()_1] method). Some properties of the [`ParseOptions`][] class (like [`Error`][]) are not used with the [`Parse()`][Parse()_5] methods, as they apply to the [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] and the static [`Parse()`][Parse()_1] methods only. Next, we'll take a look at [generating usage help](UsageHelp.md). -[`ArgumentParsed`]: https://www.ookii.org/docs/commandline-3.1/html/E_Ookii_CommandLine_CommandLineParser_ArgumentParsed.htm -[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm -[`CommandLineArgumentErrorCategory`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm -[`CommandLineArgumentException.Category`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentException_Category.htm -[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentException.htm -[`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.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm -[`CommandLineParser`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineParser_1.htm +[`ArgumentParsed`]: https://www.ookii.org/docs/commandline-4.0/html/E_Ookii_CommandLine_CommandLineParser_ArgumentParsed.htm +[`CancelMode.Abort`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm +[`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm +[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm +[`CommandLineArgumentErrorCategory`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm +[`CommandLineArgumentException.Category`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentException_Category.htm +[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentException.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser.ParseResult`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineParser_ParseResult.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser_1.htm [`Environment.GetCommandLineArgs()`]: https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs -[`Error`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_Error.htm -[`GetArgument`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_GetArgument.htm -[`HelpRequested`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm -[`ParseOptions.StringProvider`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_StringProvider.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 -[Arguments_0]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineParser_Arguments.htm -[DuplicateArgument_0]: https://www.ookii.org/docs/commandline-3.1/html/E_Ookii_CommandLine_CommandLineParser_DuplicateArgument.htm -[Parse()_5]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm -[Parse()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm -[`CommandLineParser.ParseResult`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineParser_ParseResult.htm -[`ParseResult.ArgumentName`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseResult_ArgumentName.htm -[`ParseResult.LastException`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseResult_LastException.htm -[`ParseStatus.Error`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ParseStatus.htm -[ParseWithErrorHandling()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[`Error`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_Error.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`GetArgument`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_GetArgument.htm +[`HelpRequested`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineParser_HelpRequested.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`ParseOptions.StringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_StringProvider.htm +[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptions.htm +[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm +[`ParseResult.ArgumentName`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_ArgumentName.htm +[`ParseResult.LastException`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_LastException.htm +[`ParseResult.RemainingArguments`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_RemainingArguments.htm +[`ParseStatus.Canceled`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseStatus.htm +[`ParseStatus.Error`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseStatus.htm +[`ParseStatus.Success`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseStatus.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm +[Arguments_0]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineParser_Arguments.htm +[CreateParser()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_IParserProvider_1_CreateParser.htm +[DuplicateArgument_0]: https://www.ookii.org/docs/commandline-4.0/html/E_Ookii_CommandLine_CommandLineParser_DuplicateArgument.htm +[Parse()_5]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_CommandLineParser_1_Parse.htm +[Parse()_7]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm +[Parse()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[ParseWithErrorHandling()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm diff --git a/docs/refs.json b/docs/refs.json index 760c42d8..0b953fa6 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -84,6 +84,10 @@ "CommandOptions.StripCommandNameSuffix": "P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix", "Console.WindowWidth": "#system.console.windowwidth", "CreateCommand()": "Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand", + "CreateParser()": [ + "M_Ookii_CommandLine_Commands_CommandInfo_CreateParser", + "M_Ookii_CommandLine_IParserProvider_1_CreateParser" + ], "CultureInfo": "#system.globalization.cultureinfo", "CultureInfo.InvariantCulture": "#system.globalization.cultureinfo.invariantculture", "CurrentCulture": "#system.globalization.cultureinfo.currentculture", @@ -209,7 +213,8 @@ "M_Ookii_CommandLine_CommandLineParser_Parse", "M_Ookii_CommandLine_Commands_ICommandWithCustomParsing_Parse", "Overload_Ookii_CommandLine_CommandLineParser_1_Parse", - "Overload_Ookii_CommandLine_CommandLineParser_Parse" + "Overload_Ookii_CommandLine_CommandLineParser_Parse", + "Overload_Ookii_CommandLine_IParser_1_Parse" ], "Parse()": [ "Overload_Ookii_CommandLine_CommandLineParser_Parse", @@ -238,7 +243,9 @@ "ParseResult.LastException": "P_Ookii_CommandLine_ParseResult_LastException", "ParseResult.RemainingArguments": "P_Ookii_CommandLine_ParseResult_RemainingArguments", "ParseResult.Status": "P_Ookii_CommandLine_ParseResult_Status", + "ParseStatus.Canceled": "T_Ookii_CommandLine_ParseStatus", "ParseStatus.Error": "T_Ookii_CommandLine_ParseStatus", + "ParseStatus.Success": "T_Ookii_CommandLine_ParseStatus", "ParseWithErrorHandling()": [ "M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling_1", "M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling", diff --git a/src/Ookii.CommandLine/IParser.cs b/src/Ookii.CommandLine/IParser.cs index 7c1dd180..6dff2d22 100644 --- a/src/Ookii.CommandLine/IParser.cs +++ b/src/Ookii.CommandLine/IParser.cs @@ -19,6 +19,11 @@ namespace Ookii.CommandLine; /// 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, the +/// will still generate the same methods defined by this +/// interface, just without having them implement the interface. +/// /// public interface IParser : IParserProvider where TSelf : class, IParser diff --git a/src/Ookii.CommandLine/IParserProvider.cs b/src/Ookii.CommandLine/IParserProvider.cs index 502fad45..b788f375 100644 --- a/src/Ookii.CommandLine/IParserProvider.cs +++ b/src/Ookii.CommandLine/IParserProvider.cs @@ -19,6 +19,11 @@ namespace Ookii.CommandLine; /// constructor directly; 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, the +/// will still generate the same method defined by this +/// interface, just without having it implement the interface. +/// /// public interface IParserProvider where TSelf : class, IParserProvider From 6ae1378cf1ecbfa944059876ef7f10e6e18cf801 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 16 Jun 2023 15:38:57 -0700 Subject: [PATCH 160/234] Update usage help docs including API links. --- docs/UsageHelp.md | 106 +++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/docs/UsageHelp.md b/docs/UsageHelp.md index 1de8868d..d2c60a91 100644 --- a/docs/UsageHelp.md +++ b/docs/UsageHelp.md @@ -7,10 +7,11 @@ Creating this kind of usage help text is tedious, and you must make sure it is k whenever you change the arguments to your application. Ookii.CommandLine generates this usage help text automatically, alleviating this problem. -If you use the static [`CommandLineParser.Parse()`][] method or the -[`CommandLineParser.ParseWithErrorHandling()`][] method, usage help will be printed automatically in the -event the command line is invalid, or the `-Help` argument was used. You can customize the output -using the [`ParseOptions.UsageWriter`][] property. +If you use the generated [`Parse()`][Parse()_7] method (with [source generation](SourceGeneration.md)), the +static [`CommandLineParser.Parse()`][] method, or the +[`CommandLineParser.ParseWithErrorHandling()`][] method, usage help will be printed automatically +in the event the command line is invalid, or the `-Help` argument was used. You can customize the +output using the [`ParseOptions.UsageWriter`][] property. If you don't use those methods, you can generate the usage help using the [`CommandLineParser.WriteUsage()`][] method. By default, the [`CommandLineParser.WriteUsage()`][] @@ -76,8 +77,9 @@ The description is specified by using the [`System.ComponentModel.DescriptionAtt that defines the command line arguments, as in the following example: ```csharp +[GeneratedParser] [Description("This is the application description that is included in the usage help.")] -class MyArguments +partial class MyArguments { } ``` @@ -151,9 +153,9 @@ hard to read. Set the [`UsageWriter.UseAbbreviatedSyntax`][] property to omit al positional arguments; the user can instead use the argument description list to see what arguments are available. -If you are using [long/short mode](Arguments.md), you can set the [`UsageWriter.UseShortNamesForSyntax`][] -property to use short arguments names instead of long names, for arguments that have a short name, -in the usage syntax. +If you are using [long/short mode](Arguments.md#longshort-mode), you can set the +[`UsageWriter.UseShortNamesForSyntax`][] property to use short arguments names instead of long +names, for arguments that have a short name, in the usage syntax. ### Value descriptions @@ -167,12 +169,11 @@ any namespace prefixes. For multi-value arguments the element type is used, and [`Nullable`][], the name of the type `T` is used. For dictionary arguments, the default is `TKey=TValue`. -To specify a different value description for an argument, use the -[`CommandLineArgumentAttribute.ValueDescription`][] property, or the [`ValueDescriptionAttribute`][] for -arguments defined using a constructor parameter. +To specify a different value description for an argument, use the [`ValueDescriptionAttribute`][]. ```csharp -[CommandLineArgument(ValueDescription = "Number")] +[CommandLineArgument] +[ValueDescription("Number")] public int Argument { get; set; } ``` @@ -201,10 +202,9 @@ argument description list by default. After the usage syntax, the usage help ends with a list of all arguments with their detailed descriptions. -The description of an argument can be specified using the -[`System.ComponentModel.DescriptionAttribute`][] attribute. Apply this attribute to the constructor -parameter or property defining the argument. It's strongly recommended to add a description to -every argument. +The description of an argument can be specified using the [`DescriptionAttribute`][] attribute. +Apply this attribute to the constructor parameter or property defining the argument. It's strongly +recommended to add a description to every argument. ```csharp [CommandLineArgument] @@ -228,13 +228,13 @@ You can also choose the sort order of the description list using the [`UsageWriter.ArgumentDescriptionListOrder`][] property. This defaults to the same order as the usage syntax, but you can also choose to sort by ascending or descending long or short name. -Since the static [`CommandLineParser.Parse()`][] method or the -[`CommandLineParser.ParseWithErrorHandling()`][] method method will show usage help on error, if you -have a lot of arguments it may be necessary for the user to scroll up past the argument description -list to see the error message to determine what was wrong with the command line. Since this may be -inconvenient, you can choose to omit the argument description list, or the usage help entirely, when -an error occurs, using the [`ParseOptions.ShowUsageOnError`][] property. In this case, the user will -have to use the `-Help` argument to see the full help. +The generated [`Parse()`][Parse()_7] method, the static [`CommandLineParser.Parse()`][] method and the +[`CommandLineParser.ParseWithErrorHandling()`][] method method will show usage help on error, but +by default they will show only the usage syntax, and a message telling the user to get more help +with the `-Help` argument. This makes sure the error message is obvious even if you have a lot of +arguments. + +You can use the [`ParseOptions.ShowUsageOnError`][] property to customize this behavior. ## Hidden arguments @@ -343,39 +343,39 @@ Please see the [subcommand documentation](Subcommands.md) for information about Next, we'll take a look at [argument validation and dependencies](Validation.md). -[`CommandLineArgumentAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm -[`CommandLineArgumentAttribute.ValueDescription`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ValueDescription.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 +[`CommandLineArgumentAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm +[`CommandLineParser.GetUsage()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_GetUsage.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser.WriteUsage()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_WriteUsage.htm +[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute -[`DescriptionListFilterMode.Information`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_DescriptionListFilterMode.htm -[`GetExtendedColor()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Terminal_TextFormat_GetExtendedColor.htm +[`DescriptionListFilterMode.Information`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_DescriptionListFilterMode.htm +[`GetExtendedColor()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Terminal_TextFormat_GetExtendedColor.htm [`Int32`]: https://learn.microsoft.com/dotnet/api/system.int32 -[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm [`Nullable`]: https://learn.microsoft.com/dotnet/api/system.nullable-1 -[`ParseOptions.DefaultValueDescriptions`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_DefaultValueDescriptions.htm -[`ParseOptions.ShowUsageOnError`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_ShowUsageOnError.htm -[`ParseOptions.UsageWriter`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_UsageWriter.htm +[`ParseOptions.DefaultValueDescriptions`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_DefaultValueDescriptions.htm +[`ParseOptions.ShowUsageOnError`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ShowUsageOnError.htm +[`ParseOptions.UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_UsageWriter.htm [`SetConsoleMode`]: https://learn.microsoft.com/windows/console/setconsolemode [`String`]: https://learn.microsoft.com/dotnet/api/system.string [`System.ComponentModel.DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute -[`TextFormat`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Terminal_TextFormat.htm -[`UsageWriter.ArgumentDescriptionListFilter`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListFilter.htm -[`UsageWriter.ArgumentDescriptionListOrder`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListOrder.htm -[`UsageWriter.IncludeApplicationDescription`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescription.htm -[`UsageWriter.UseAbbreviatedSyntax`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_UseAbbreviatedSyntax.htm -[`UsageWriter.UseColor`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_UseColor.htm -[`UsageWriter.UseShortNamesForSyntax`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_UseShortNamesForSyntax.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_UsageWriter.htm -[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm -[`WriteArgumentDescriptions()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentDescriptions.htm -[`WriteArgumentName()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentName.htm -[`WriteArgumentSyntax()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentSyntax.htm -[`WriteParserUsageCore()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteParserUsageCore.htm -[`WriteParserUsageSyntax()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteParserUsageSyntax.htm -[`WriteValueDescription()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteValueDescription.htm -[`WriteValueDescriptionForDescription()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteValueDescriptionForDescription.htm -[WriteArgumentDescription()_1]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentDescription.htm -[`CommandLineParser.GetUsage()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_GetUsage.htm -[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[`TextFormat`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Terminal_TextFormat.htm +[`UsageWriter.ArgumentDescriptionListFilter`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListFilter.htm +[`UsageWriter.ArgumentDescriptionListOrder`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListOrder.htm +[`UsageWriter.IncludeApplicationDescription`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescription.htm +[`UsageWriter.UseAbbreviatedSyntax`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_UseAbbreviatedSyntax.htm +[`UsageWriter.UseColor`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_UseColor.htm +[`UsageWriter.UseShortNamesForSyntax`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_UseShortNamesForSyntax.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm +[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm +[`WriteArgumentDescriptions()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentDescriptions.htm +[`WriteArgumentName()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentName.htm +[`WriteArgumentSyntax()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentSyntax.htm +[`WriteParserUsageCore()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteParserUsageCore.htm +[`WriteParserUsageSyntax()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteParserUsageSyntax.htm +[`WriteValueDescription()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteValueDescription.htm +[`WriteValueDescriptionForDescription()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteValueDescriptionForDescription.htm +[Parse()_7]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm +[WriteArgumentDescription()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteArgumentDescription.htm From c0bb0b8c39ec93074e8ad5e0194085b2af469296 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 16 Jun 2023 15:52:50 -0700 Subject: [PATCH 161/234] Updated validation docs, including API links. --- docs/DefiningArguments.md | 5 +++ docs/Validation.md | 94 +++++++++++++++++++++++---------------- docs/refs.json | 2 + 3 files changed, 62 insertions(+), 39 deletions(-) diff --git a/docs/DefiningArguments.md b/docs/DefiningArguments.md index 3c3b9974..c1a8411d 100644 --- a/docs/DefiningArguments.md +++ b/docs/DefiningArguments.md @@ -319,6 +319,10 @@ public int Argument { get; set; } 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. @@ -638,6 +642,7 @@ Next, we'll take a look at how to [parse the arguments we've defined](ParsingArg [`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 diff --git a/docs/Validation.md b/docs/Validation.md index abf51ea8..cf8e9b7f 100644 --- a/docs/Validation.md +++ b/docs/Validation.md @@ -6,10 +6,11 @@ range. While it's possible to do this kind of validation after the arguments have been parsed, or to write custom property setters that perform the validation, Ookii.CommandLine also provides validation -attributes. The advantage of this is that you can reuse common validation rules, if you use the -static [`CommandLineParser.Parse()`][] or [`CommandLineParser.ParseWithErrorHandling()`][] method -it will handle printing validation error messages, and validators can also add a help message to the -argument descriptions in the [usage help](UsageHelp.md). +attributes. The advantage of this is that you can reuse common validation rules, if you use one of +the generated [`Parse()`][Parse()_7], static [`CommandLineParser.Parse()`][] or +[`CommandLineParser.ParseWithErrorHandling()`][] methods it will handle printing validation error +messages, and validators can also add a help message to the argument descriptions in the [usage +help](UsageHelp.md). ## Built-in validators @@ -24,7 +25,7 @@ are discussed [below](#argument-dependencies-and-restrictions)): Validator | Description | Applied -------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------- [`ValidateCountAttribute`][] | Validates that the number of items for a multi-value argument is in the specified range. | After parsing. -[`ValidateEnumValueAttribute`][] | Validates that the value is one of the defined values for an enumeration. The default [`TypeConverter`][] for an enumeration allows conversion from the underlying value, even if that value is not a defined value for the enumeration. This validator prevents that. See also [enumeration type conversion](Arguments.md#enumeration-type-conversion). | After conversion. +[`ValidateEnumValueAttribute`][] | Validates that the value is one of the defined values for an enumeration. The default [`TypeConverter`][] for an enumeration allows conversion from the underlying value, even if that value is not a defined value for the enumeration. This validator prevents that. See also [enumeration type conversion](Arguments.md#enumeration-conversion). | After conversion. [`ValidateNotEmptyAttribute`][] | Validates that the value of an argument is not an empty string. | Before conversion. [`ValidateNotNullAttribute`][] | Validates that the value of an argument is not null. This is only useful if the [`TypeConverter`][] for an argument can return null (for example, the [`NullableConverter`][] can). It's not necessary to use this validator on non-nullable value types, or if using .Net 6.0 or later, on non-nullable reference types. | After conversion. [`ValidateNotWhiteSpaceAttribute`][] | Validates that the value of an argument is not an empty string or a string containing only white-space characters. | Before conversion. @@ -35,7 +36,7 @@ Validator | Description Note that there is no `ValidateSetAttribute`, or an equivalent way to make sure that an argument is one of a predefined set of values, because you're encouraged to use an enumeration type for this instead, in combination with the [`ValidateEnumValueAttribute`][] if desired. You can of course use -the [`ValidatePatternAttribute`][] for this purpose as well. +the [`ValidatePatternAttribute`][] for this purpose as well, or you can create a custom validator. The [`ValidateRangeAttribute`][], [`ValidateCountAttribute`][] and [`ValidateStringLengthAttribute`][] all allow the use of open-ended ranges, without either a lower @@ -142,8 +143,9 @@ For example, you might have an application that can read data from a file, or fr specified IP address and port. You could express these arguments as follows: ```csharp +[GeneratedParser] [RequiresAny(nameof(Path), nameof(Ip))] -internal class ProgramArguments +partial class ProgramArguments { [CommandLineArgument(Position = 0)] [Description("The path to use.")] @@ -244,42 +246,56 @@ class ValidateFutureDateAttribute : ValidateRangeAttribute } ``` +### Validation using ReadOnlySpan\ + +If an argument is provided using the name/value separator (e.g. `-Argument:value`), the +[`CommandLineParser`][] class will try to avoid allocating a new string for the value as long as the +argument converter and any pre-conversion validators support using a [`ReadOnlySpan`][]. For +this reason, it's strongly recommended that you implement the +[`ArgumentValidationAttribute.IsSpanValid`][] method for a custom pre-conversion validator. This +does not apply to validators that don't use [`ValidationMode.BeforeConversion`][]. + Now that you know (almost) everything there is to know about arguments, let's move on to [subcommands](Subcommands.md). -[`ArgumentValidationAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ArgumentValidationAttribute.htm -[`ArgumentValidationWithHelpAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute.htm -[`Category`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_CommandLineArgumentException_Category.htm -[`ClassValidationAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ClassValidationAttribute.htm -[`CommandLineArgumentErrorCategory.ValidationFailed`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm -[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentException.htm -[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`ArgumentValidationAttribute.IsSpanValid`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsSpanValid.htm +[`ArgumentValidationAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ArgumentValidationAttribute.htm +[`ArgumentValidationWithHelpAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute.htm +[`Category`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentException_Category.htm +[`ClassValidationAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ClassValidationAttribute.htm +[`CommandLineArgumentErrorCategory.ValidationFailed`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm +[`CommandLineArgumentException`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentException.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm [`DateOnly`]: https://learn.microsoft.com/dotnet/api/system.dateonly -[`ErrorCategory`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_ErrorCategory.htm -[`GetErrorMessage()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetErrorMessage.htm -[`GetUsageHelp()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetUsageHelp.htm -[`GetUsageHelpCore()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_GetUsageHelpCore.htm +[`ErrorCategory`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_ErrorCategory.htm +[`GetErrorMessage()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetErrorMessage.htm +[`GetUsageHelp()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetUsageHelp.htm +[`GetUsageHelpCore()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_GetUsageHelpCore.htm [`IComparable`]: https://learn.microsoft.com/dotnet/api/system.icomparable-1 -[`IsValid()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsValid.htm -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`IsValid()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsValid.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm [`NullableConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.nullableconverter -[`Ookii.CommandLine.Validation`]: https://www.ookii.org/docs/commandline-3.1/html/N_Ookii_CommandLine_Validation.htm -[`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 +[`Ookii.CommandLine.Validation`]: https://www.ookii.org/docs/commandline-4.0/html/N_Ookii_CommandLine_Validation.htm +[`ProhibitsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ProhibitsAttribute.htm +[`ReadOnlySpan`]: https://learn.microsoft.com/dotnet/api/system.readonlyspan-1 +[`RequiresAnyAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_RequiresAnyAttribute.htm +[`RequiresAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_RequiresAttribute.htm [`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter -[`UsageWriter.IncludeValidatorsInDescription`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_IncludeValidatorsInDescription.htm -[`ValidateCountAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateCountAttribute.htm -[`ValidateEnumValueAttribute.IncludeValuesInErrorMessage`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_IncludeValuesInErrorMessage.htm -[`ValidateEnumValueAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateEnumValueAttribute.htm -[`ValidateNotEmptyAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateNotEmptyAttribute.htm -[`ValidateNotNullAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateNotNullAttribute.htm -[`ValidateNotWhiteSpaceAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateNotWhiteSpaceAttribute.htm -[`ValidatePatternAttribute.ErrorMessage`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Validation_ValidatePatternAttribute_ErrorMessage.htm -[`ValidatePatternAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidatePatternAttribute.htm -[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm -[`ValidateStringLengthAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Validation_ValidateStringLengthAttribute.htm -[IncludeInUsageHelp_0]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_IncludeInUsageHelp.htm -[Mode_3]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_Mode.htm -[ValidationFailed_1]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm -[`CommandLineParser.ParseWithErrorHandling()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling.htm +[`UsageWriter.IncludeValidatorsInDescription`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeValidatorsInDescription.htm +[`ValidateCountAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateCountAttribute.htm +[`ValidateEnumValueAttribute.IncludeValuesInErrorMessage`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_IncludeValuesInErrorMessage.htm +[`ValidateEnumValueAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateEnumValueAttribute.htm +[`ValidateNotEmptyAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateNotEmptyAttribute.htm +[`ValidateNotNullAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateNotNullAttribute.htm +[`ValidateNotWhiteSpaceAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateNotWhiteSpaceAttribute.htm +[`ValidatePatternAttribute.ErrorMessage`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ValidatePatternAttribute_ErrorMessage.htm +[`ValidatePatternAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidatePatternAttribute.htm +[`ValidateRangeAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateRangeAttribute.htm +[`ValidateStringLengthAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateStringLengthAttribute.htm +[`ValidationMode.BeforeConversion`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidationMode.htm +[IncludeInUsageHelp_0]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute_IncludeInUsageHelp.htm +[Mode_3]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_Mode.htm +[Parse()_7]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm +[ValidationFailed_1]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentErrorCategory.htm diff --git a/docs/refs.json b/docs/refs.json index 0b953fa6..ba548eed 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -22,6 +22,7 @@ "Arguments.Parse()": null, "ArgumentType": "P_Ookii_CommandLine_CommandLineArgument_ArgumentType", "ArgumentValidationAttribute": "T_Ookii_CommandLine_Validation_ArgumentValidationAttribute", + "ArgumentValidationAttribute.IsSpanValid": "M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsSpanValid", "ArgumentValidationWithHelpAttribute": "T_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute", "AssemblyTitleAttribute": "#system.reflection.assemblytitleattribute", "AsyncCommandBase": "T_Ookii_CommandLine_Commands_AsyncCommandBase", @@ -322,6 +323,7 @@ "M_Ookii_CommandLine_LocalizedStringProvider_ValidationFailed", "T_Ookii_CommandLine_CommandLineArgumentErrorCategory" ], + "ValidationMode.BeforeConversion": "T_Ookii_CommandLine_Validation_ValidationMode", "ValueConverterAttribute": "T_Ookii_CommandLine_Conversion_ValueConverterAttribute", "ValueDescriptionAttribute": "T_Ookii_CommandLine_ValueDescriptionAttribute", "VirtualTerminal": "T_Ookii_CommandLine_Terminal_VirtualTerminal", From cd56c35115533af30edc13828d39427bb0069bac Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 11:45:55 -0700 Subject: [PATCH 162/234] Updated sub command documentation. --- docs/Subcommands.md | 281 +++++++++++++++++++++++++++++--------------- 1 file changed, 189 insertions(+), 92 deletions(-) diff --git a/docs/Subcommands.md b/docs/Subcommands.md index 6ae31728..3279f75a 100644 --- a/docs/Subcommands.md +++ b/docs/Subcommands.md @@ -6,13 +6,12 @@ uses it with commands like `dotnet build` and `dotnet run`, as does `git` with c `git pull` and `git cherry-pick`. Ookii.CommandLine makes it trivial to define and use subcommands, using the same techniques we've -already seen for defining and parsing arguments. Subcommand specific functionality is all in the +already seen for defining and parsing arguments. Subcommand-specific functionality is all in the [`Ookii.CommandLine.Commands`][] namespace. -In an application using subcommands, the first argument to the application is the name of the -command. The remaining arguments are arguments to that command. You cannot have arguments that are -not associated with a command using the subcommand functionality in Ookii.CommandLine, though you -can still easily define [common arguments](#multiple-commands-with-common-arguments). +In an application using subcommands, the first argument to the application is typically the name of +the command. The remaining arguments are arguments to that command. Sometimes, there are also +arguments that are [common to all commands](#multiple-commands-with-common-arguments). For example, the [subcommand sample](../src/Samples/Subcommand) can be invoked as follows: @@ -26,8 +25,7 @@ command. ## Defining subcommands A subcommand class is essentially the same as a [regular arguments class](DefiningArguments.md). -Arguments can be defined using its constructor parameters, properties, and methods, exactly as was -shown before. +Arguments can be defined using its properties and methods, exactly as was shown before. Subcommand classes have the following differences from regular arguments classes: @@ -42,13 +40,14 @@ Subcommand classes have the following differences from regular arguments classes It's therefore trivial to take any arguments class, and convert it into a subcommand: ```csharp +[GeneratedParser] [Command("sample")] [Description("This is a sample command.")] -class SampleCommand : ICommand +partial class SampleCommand : ICommand { - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true)] [Description("A sample argument for the sample command.")] - public string? SampleArgument { get; set; } + public required string SampleArgument { get; set; } public int Run() { @@ -61,17 +60,18 @@ class SampleCommand : ICommand This code creates a subcommand which can be invoked with the name `sample`, and which has a single positional required argument. -The [`ICommand`][] interface defines a single method, [`ICommand.Run()`][], which all subcommands must -implement. This function is invoked to run your command. The return value is typically used as the -exit code for the application, after the command finishes running. +The [`ICommand`][] interface defines a single method, [`ICommand.Run()`][], which all subcommands +must implement. This function is invoked to run your command. The return value is typically used as +the exit code for the application, after the command finishes running. When using the [`CommandManager`][] class as [shown below](#using-subcommands), the class will be -created using the [`CommandLineParser`][] as usual, using all the arguments except for the command name. -Then, the [`ICommand.Run()`][] method will be called. +created using the [`CommandLineParser`][] as usual, using all the arguments except for the command +name. Then, the [`ICommand.Run()`][] method will be called. All of the functionality and [options](#subcommand-options) available with regular arguments types are available with commands too, including [usage help generation](#subcommand-usage-help), -[long/short mode](Arguments.md#longshort-mode), all kinds of arguments, validators, etc. +[long/short mode](Arguments.md#longshort-mode), all kinds of arguments, validators, source +generation, etc. ### Name transformation @@ -79,8 +79,9 @@ The sample above used the [`CommandAttribute`][] attribute to set an explicit na no name is specified, the name is derived from the type name. ```csharp +[GeneratedParser] [Command] -class ReadDirectoryCommand : ICommand +partial class ReadDirectoryCommand : ICommand { /* omitted */ } @@ -94,9 +95,9 @@ to command names. This is done by setting the [`CommandOptions.CommandNameTransf names. In addition to just transforming the case and separators, command name transformation can also strip -a suffix from the end of the type name. This is set with the [`CommandOptions.StripCommandNameSuffix`][] -property, and defaults to "Command". This is only used if the [`CommandNameTransform`][] is not -[`NameTransform.None`][]. +a suffix from the end of the type name. This is set with the +[`CommandOptions.StripCommandNameSuffix`][] property, and defaults to "Command". This is only used +if the [`CommandNameTransform`][] is not [`NameTransform.None`][]. So, if you use the [`NameTransform.DashCase`][] transform, with the default [`StripCommandNameSuffix`][] value, the `ReadDirectoryCommand` class above will create a command named `read-directory`. @@ -107,37 +108,52 @@ Like argument names, a command can have one or more aliases, alternative names t to invoke the command. Simply apply the [`AliasAttribute`][] to the command class. ```csharp +[GeneratedParser] [Command] [Alias("ls")] -class ReadDirectoryCommand : ICommand +partial class ReadDirectoryCommand : ICommand { /* omitted */ } ``` +Command names also use automatic prefix aliases by default, so any prefix that uniquely identifies a +command by its name or one of its explicit aliases can be used to invoke that command. + +For example, with two commands `read` and `record`, the prefix `rea` would be an alias for the +`read` command, and `rec`, `reco` and `recor` are automatic aliases of the `record` command. The +prefixes `r` and `re` are not automatic aliases, because they are ambiguous between the two +commands. + +Automatic prefix aliases for command names can be disabled using the +`CommandOptions.AutoCommandPrefixAliases` property. + ### Asynchronous commands -It's possible to use asynchronous code with subcommands. To do this, implement the [`IAsyncCommand`][] -interface, which derives from [`ICommand`][], and use the [`CommandManager.RunCommandAsync()`][] method (see -[below](#using-subcommands)). +It's possible to use asynchronous code with subcommands. To do this, implement the +[`IAsyncCommand`][] interface, which derives from [`ICommand`][], and use the +[`CommandManager.RunCommandAsync()`][] method (see [below](#using-subcommands)). The [`IAsyncCommand`][] interface adds a new [`IAsyncCommand.RunAsync()`][] method, but because -[`IAsyncCommand`][] derives from [`ICommand`][], it's still necessary to implement the [`ICommand.Run()`][] -method. If you use [`RunCommandAsync()`][], the [`ICommand.Run()`][] method is guaranteed to never be called -on a command that implements [`IAsyncCommand`][], so you can just leave this empty. +[`IAsyncCommand`][] derives from [`ICommand`][], it's still necessary to implement the +[`ICommand.Run()`][] method. If you use [`RunCommandAsync()`][], the [`ICommand.Run()`][] method is +guaranteed to never be called on a command that implements [`IAsyncCommand`][], so you can just +leave this empty. -However, a better option is to use the [`AsyncCommandBase`][] class, which is provided for convenience, -and provides an implementation of [`ICommand.Run()`][] which invokes [`IAsyncCommand.RunAsync()`][] and -waits for it. That way, your command is compatible with both [`RunCommand()`][] and [`RunCommandAsync()`][]. +However, a better option is to use the [`AsyncCommandBase`][] class, which is provided for +convenience, and provides an implementation of [`ICommand.Run()`][] which invokes +[`IAsyncCommand.RunAsync()`][] and waits for it. That way, your command is compatible with both +[`RunCommand()`][] and [`RunCommandAsync()`][]. ```csharp +[GeneratedParser] [Command] [Description("Sleeps for a specified amount of time.")] -class AsyncSleepCommand : AsyncCommandBase +partial class AsyncSleepCommand : AsyncCommandBase { - [CommandLineArgument(Position = 0, DefaultValue = 1000)] + [CommandLineArgument(IsPositional = true)] [Description("The sleep time in milliseconds.")] - public int SleepTime { get; set; }; + public int SleepTime { get; set; } = 1000; public override async Task RunAsync() { @@ -151,8 +167,8 @@ class AsyncSleepCommand : AsyncCommandBase You may have multiple commands that have one or more arguments in common. For example, you may have a database application where every command needs the connection string as an argument. Because -[`CommandLineParser`][] considers base class members when defining arguments, this can be accomplished -by having a common base class for each command that needs the common arguments. +[`CommandLineParser`][] considers base class members when defining arguments, this can be +accomplished by having a common base class for each command that needs the common arguments. ```csharp abstract class DatabaseCommand : ICommand @@ -163,8 +179,9 @@ abstract class DatabaseCommand : ICommand public abstract int Run(); } +[GeneratedParser] [Command] -class AddCommand : DatabaseCommand +partial class AddCommand : DatabaseCommand { [CommandLineArgument(Position = 1, IsRequired = true)] public string? NewValue { get; set; } @@ -175,8 +192,9 @@ class AddCommand : DatabaseCommand } } +[GeneratedParser] [Command] -class DeleteCommand : DatabaseCommand +partial class DeleteCommand : DatabaseCommand { [CommandLineArgument(Position = 1, IsRequired = true)] public int Id { get; set; } @@ -195,21 +213,36 @@ The two commands, `AddCommand` and `DeleteCommand` both inherit the `-Connection add their own additional arguments. The `DatabaseCommand` class is not considered a subcommand by the [`CommandManager`][], because it -does not have the [`CommandAttribute`][] attribute, and because it is abstract. +does not have the [`CommandAttribute`][] attribute, and because it is abstract. It also does not +need the `GeneratedParserAttribute`, because the attribute on the derived classes will process the +base class arguments. + +Some applications also have options that don't belong to any specific command, but can instead be +specified before the command name. The default behavior of Ookii.CommandLine treats the first +argument as the command name, but it is possible to build an application where this is not the case. + +To do so, you need to define an arguments class (not a subcommand) that defines the top-level +arguments, one of which (typically the last positional argument) is the command name. That argument +should set the `CommandLineArgumentAttribute.CancelParsing` property to `CancelMode.Success`. After +parsing the arguments for this class, you can then invoke the [`CommandManager`][] using the command +name from that argument, and the remaining arguments from the `ParseResult.RemainingArguments` +property. + +An example of how to do this can be found in the [top-level arguments sample](../src/Samples/TopLevelArguments). ### Custom parsing -In some cases, you may want to create commands that do not use the [`CommandLineParser`][] class to parse -their arguments. For this purpose, you can implement the [`ICommandWithCustomParsing`][] method instead. -You must still use the [`CommandAttribute`][]. +In some cases, you may want to create commands that do not use the [`CommandLineParser`][] class to +parse their arguments. For this purpose, you can implement the [`ICommandWithCustomParsing`][] +method instead. You must still use the [`CommandAttribute`][]. Your type must have a constructor with no parameters, and implement the -[`ICommandWithCustomParsing.Parse()`][] method, which will be called before [`ICommand.Run()`][] to allow -you to parse the command line arguments. You can combine [`ICommandWithCustomParsing`][] with +[`ICommandWithCustomParsing.Parse()`][] method, which will be called before [`ICommand.Run()`][] to +allow you to parse the command line arguments. You can combine [`ICommandWithCustomParsing`][] with [`IAsyncCommand`][] if you wish. In this case, it is up to the command to handle argument parsing, and handle errors and display -usage help if appropriate. +usage help if appropriate. Source generation cannot be used with a command that uses custom parsing. For example, you may have a command that launches an external executable, and wants to pass the arguments to that executable. @@ -218,11 +251,11 @@ arguments to that executable. [Command] class LaunchCommand : AsyncCommandBase, ICommandWithCustomParsing { - private string[]? _args; + private ReadOnlyMemory _args; - public void Parse(string[] args, int index, CommandOptions options) + public void Parse(ReadOnlyMemory args, CommandManager manager) { - _args = args[index..]; + _args = args; } public override async Task RunAsync() @@ -292,14 +325,14 @@ public static async Task Main() } ``` -Note that the [`RunCommandAsync()`][] method can still run commands that only implement [`ICommand`][], and -not [`IAsyncCommand`][], so you can freely mix both types of command. +Note that the [`RunCommandAsync()`][] method can still run commands that only implement +[`ICommand`][], and not [`IAsyncCommand`][], so you can freely mix both types of command. -If you use [`RunCommand()`][] with asynchronous commands, it will call the [`ICommand.Run()`][] method, so -whether this works depends on the command's implementation of that method. If you used -[`AsyncCommandBase`][], this will call the [`RunAsync()`][RunAsync()_0] method, so the command will work correctly. -However, in all cases, it's strongly recommended to use [`RunCommandAsync()`][] if you use any -asynchronous commands. +If you use [`RunCommand()`][] with asynchronous commands, it will call the [`ICommand.Run()`][] +method, so whether this works depends on the command's implementation of that method. If you used +[`AsyncCommandBase`][], this will call the [`RunAsync()`][RunAsync()_0] method, so the command will +work correctly. However, in all cases, it's strongly recommended to use [`RunCommandAsync()`][] if +you use any asynchronous commands. Check out the [tutorial](Tutorial.md) and the [subcommand sample](../src/Samples/Subcommand) for more detailed examples of how to create and use commands. @@ -329,6 +362,38 @@ public static int Main() The omitted `LoadPlugins()` method would presumably load some list of assemblies from the application's configuration. +### Using source generation with subcommands + +While the `GeneratedParserAttribute` can be applied to commands, and the generated parser will be +used by the [`CommandManager`][] class, the [`CommandManager`][] class still uses reflection to +find the subcommand classes in the specified assemblies. + +To use [source generation](SourceGeneration.md#generating-a-command-manager) to find the commands at +compile time and provide that information to a generated command manager, you must define a class as +follows, using the `GeneratedCommandManagerAttribute`: + +```csharp +[GeneratedCommandManager] +partial class GeneratedManager +{ +} +``` + +The source generator will make this class inherit from [`CommandManager`][], so it can be used as +a drop-in replacement for [`CommandManager`][]. + +```csharp +public static async Task Main() +{ + var manager = new GeneratedManager(); + return await manager.RunCommandAsync() ?? 1; +} +``` + +In this case, if you want to use commands from other assemblies, you must specify them using the +`GeneratedCommandManagerAttribute`, and they can only come from assemblies that are directly +referenced from your application, not dynamically loaded ones. + ### Subcommand options Just like when you use [`CommandLineParser`][] directly, there are many options available to @@ -347,22 +412,21 @@ public static int Main() { var options = new CommandOptions() { - CommandNameComparer = StringComparer.InvariantCulture, + CommandNameComparison = StringComparison.InvariantCulture, CommandNameTransform = NameTransform.DashCase, UsageWriter = new UsageWriter() { - IncludeCommandHelpInstruction = true, IncludeApplicationDescriptionBeforeCommandList = true, } }; - var manager = new CommandManager(options); + var manager = new CommandManager(options); // or a generated command manager. return manager.RunCommand() ?? 1; } ``` This code makes command names case sensitive by using the invariant string comparer (the default is -[`StringComparer.OrdinalIgnoreCase`][], which is case insensitive), enables a name transformation, +`StringComparison.OrdinalIgnoreCase`, which is case insensitive), enables a name transformation, and also sets some [usage help options](#subcommand-usage-help). ### Custom error handling @@ -372,11 +436,11 @@ As with the static [`CommandLineParser.Parse()`][] method, [`RunCommand()`][] manually, [`CommandManager`][] provides the tools to do so. If you only want more information about the error, but still want the [`CommandManager`][] class to -handle and display errors and usage help, you can check the [`CommandManager.ParseResult`][] property to -get information if [`RunCommand()`][] or [`RunCommandAsync()`][] returned null. The value of the -[`ParseResult.Status`][] property of the returned structure will indicate whether the command was not -found, if an error occurred parsing the command's arguments, or if parsing was canceled by one of -the command's arguments. +handle and display errors and usage help, you can check the [`CommandManager.ParseResult`][] +property to get information if [`RunCommand()`][] or [`RunCommandAsync()`][] returned null. The +value of the [`ParseResult.Status`][] property of the returned structure will indicate whether the +command was not found, if an error occurred parsing the command's arguments, or if parsing was +canceled by one of the command's arguments. If you want to handle errors entirely manually, the [`CommandManager.GetCommand()`][] method returns information about a command, if one with the specified name exists. From there, you can manually @@ -397,7 +461,7 @@ var options = new CommandOptions() UsageWriter = new UsageWriter(writer), }; -var manager = new CommandManager(options); +var manager = new CommandManager(options); // or a generated command manager. var exitCode = await manager.RunCommandAsync(); if (exitCode is int value) { @@ -422,9 +486,9 @@ look like. public static async Task Main(string[] args) { var options = new CommandOptions() { /* omitted */ }; - var manager = new CommandManager(options); - var info = args.Length > 0 ? manager.GetCommand(args[0]) : null; - if (info is not CommandInfo commandInfo) + var manager = new CommandManager(options); // or a generated command manager. + var commandInfo = args.Length > 0 ? manager.GetCommand(args[0]) : null; + if (commandInfo == null) { // No command or unknown command. manager.WriteUsage(); @@ -434,10 +498,10 @@ public static async Task Main(string[] args) ICommand? command = null; if (commandInfo.UseCustomArgumentParsing) { - // CreateInstance handles parsing errors and displays usage when it uses the - // CommandLineParser, so don't use it in that case. However, it must be used for commands - // with custom parsing. How errors are handled here depends on the command. - command = commandInfo.CreateInstance(args, 1); + // Invoke the custom parsing method; how errors are handled depends on the command here. + command = commandInfo.CreateInstanceWithCustomParsing(); + // Skip the command name in the arguments. + command.Parse(args.AsMemory(1), manager); } else { @@ -445,7 +509,7 @@ public static async Task Main(string[] args) try { // Skip the command name in the arguments. - command = (ICommand?)parser.Parse(args, 1); + command = (ICommand?)parser.Parse(args.AsMemory(1)); } catch (CommandLineArgumentException ex) { @@ -480,8 +544,9 @@ usage help automatically. ## Subcommand usage help Since subcommands are created using the [`CommandLineParser`][], they support showing usage help -when parsing errors occur, or the `-Help` argument is used. For example, with the [subcommand sample](../src/Samples/Subcommand) -you could run the following to get help on the `read` command: +when parsing errors occur, or the `-Help` argument is used. For example, with the +[subcommand sample](../src/Samples/Subcommand) you could run the following to get help on the `read` +command: ```text ./Subcommand read -help @@ -512,17 +577,17 @@ Run 'Subcommand -Help' for more information about a command. ``` Usage help for a [`CommandManager`][] is also created using the [`UsageWriter`][], and can be -customized by setting the subcommand-specific properties of that class. The sample above uses two of +customized by setting the subcommand-specific properties of that class. The sample above uses one of them: [`IncludeApplicationDescriptionBeforeCommandList`][], which causes the assembly description of -the first assembly used by the [`CommandManager`][] to be printed before the command list, and -[`IncludeCommandHelpInstruction`][], which prints the line at the bottom telling the user to use -`-Help`. +the first assembly used by the [`CommandManager`][] to be printed before the command list. + +The usage help will show information at the bottom on how to get help for each command, using the +name of the automatic help argument. This message will only be shown if the automatic help argument +is enabled, none of the commands use custom parsing, and all commands use the same parsing mode, +argument name transformation, and argument name prefixes. -For the [`IncludeCommandHelpInstruction`][] option, the text will use the name of the automatic help -argument, after applying the [`ParseOptions.ArgumentNameTransform`][] if one is set. If using -[long/short mode](Arguments.md#longshort-mode), the long argument prefix is used. Note that the -[`CommandManager`][] won't check if every command actually has an argument with that name, so only -enable it if this is true (it's recommended to enable it if possible). +You can force or disable the inclusion of the command help instruction by using the +`UsageWriter.IncludeCommandHelpInstruction` property. Other properties let you configure indentation and colors, among others. @@ -549,14 +614,49 @@ description of the command can be customized using the [`LocalizedStringProvider ## Nested subcommands -Ookii.CommandLine does not natively support nested subcommands. However, with the -[`CommandOptions.CommandFilter`][] property and the [`ICommandWithCustomParsing`][] interface, it provides -the tools needed to implement support for this fairly easily. +Ookii.CommandLine supports nested subcommands through the `ParentCommandAttribute`, the +`ParentCommand` class, and the `CommandOptions.ParentCommand` property. The [`CommandManager`][] +will only return commands whose `ParentCommandAttribute` matches the type specified in the +`CommandOptions.ParentCommand` property. By default, this property is null, so commands that do not +have a parent command will be returned. -The [nested commands sample](../src/Samples/NestedCommands) shows a complete implementation of this -functionality. +To create a command that can have nested commands, the easiest way is to create a class that derives +from the `ParentCommand` class. + +```csharp +[Command] +[Description("A command with nested subcommands.")] +class MyParentCommand : ParentCommand +{ +} +``` + +> `ParentCommand` uses `ICommandWithCustomParsing`, so it cannot use the `GeneratedParserAttribute`. + +Typically, this class can be empty, although `ParentCommand` provides several protected methods you +can override to customize the behavior. -Providing native support for nested subcommands is planned for a future release. +To define a command that is nested under `MyParentCommand`, you need to use the +`ParentCommandAttribute`. + +```csharp +[GeneratedParser] +[Command] +[ParentCommand(typeof(MyParentCommand))] +partial class ChildCommand : ICommand +{ + // Omitted. +} +``` + +When run, `MyParentCommand` will modify the `CommandOptions.ParentCommand` property and use the +[`CommandManager`][] again to find and execute the nested commands. + +Note that the automatic version command has no parent and will therefore only exist at the top +level. + +The [nested commands sample](../src/Samples/NestedCommands) shows a an example of how to use this +functionality. The next page will discuss Ookii.CommandLine's [source generation](SourceGeneration.md) in more detail. @@ -586,19 +686,16 @@ detail. [`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 [`IncludeApplicationDescriptionBeforeCommandList`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList.htm -[`IncludeCommandHelpInstruction`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_IncludeCommandHelpInstruction.htm [`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm [`NameTransform.DashCase`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_NameTransform.htm [`NameTransform.None`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_NameTransform.htm [`Ookii.CommandLine.Commands`]: https://www.ookii.org/docs/commandline-3.1/html/N_Ookii_CommandLine_Commands.htm [`ParseOptions.AutoVersionArgument`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_AutoVersionArgument.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 [`RunCommand()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm [`RunCommand`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm [`RunCommandAsync()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm -[`StringComparer.OrdinalIgnoreCase`]: https://learn.microsoft.com/dotnet/api/system.stringcomparer.ordinalignorecase [`StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm [`UsageWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_UsageWriter.htm [`WriteCommandDescription()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteCommandDescription.htm From 2dd5aec3f89b9011f247b07403dec8954ada9963 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 11:51:29 -0700 Subject: [PATCH 163/234] Update sub command docs API links. --- docs/Subcommands.md | 131 ++++++++++++++++++++++++-------------------- docs/refs.json | 13 +++++ 2 files changed, 84 insertions(+), 60 deletions(-) diff --git a/docs/Subcommands.md b/docs/Subcommands.md index 3279f75a..109ec6f8 100644 --- a/docs/Subcommands.md +++ b/docs/Subcommands.md @@ -126,7 +126,7 @@ prefixes `r` and `re` are not automatic aliases, because they are ambiguous betw commands. Automatic prefix aliases for command names can be disabled using the -`CommandOptions.AutoCommandPrefixAliases` property. +[`CommandOptions.AutoCommandPrefixAliases`][] property. ### Asynchronous commands @@ -214,7 +214,7 @@ add their own additional arguments. The `DatabaseCommand` class is not considered a subcommand by the [`CommandManager`][], because it does not have the [`CommandAttribute`][] attribute, and because it is abstract. It also does not -need the `GeneratedParserAttribute`, because the attribute on the derived classes will process the +need the [`GeneratedParserAttribute`][], because the attribute on the derived classes will process the base class arguments. Some applications also have options that don't belong to any specific command, but can instead be @@ -223,9 +223,9 @@ argument as the command name, but it is possible to build an application where t To do so, you need to define an arguments class (not a subcommand) that defines the top-level arguments, one of which (typically the last positional argument) is the command name. That argument -should set the `CommandLineArgumentAttribute.CancelParsing` property to `CancelMode.Success`. After +should set the [`CommandLineArgumentAttribute.CancelParsing`][] property to [`CancelMode.Success`][]. After parsing the arguments for this class, you can then invoke the [`CommandManager`][] using the command -name from that argument, and the remaining arguments from the `ParseResult.RemainingArguments` +name from that argument, and the remaining arguments from the [`ParseResult.RemainingArguments`][] property. An example of how to do this can be found in the [top-level arguments sample](../src/Samples/TopLevelArguments). @@ -364,13 +364,13 @@ application's configuration. ### Using source generation with subcommands -While the `GeneratedParserAttribute` can be applied to commands, and the generated parser will be +While the [`GeneratedParserAttribute`][] can be applied to commands, and the generated parser will be used by the [`CommandManager`][] class, the [`CommandManager`][] class still uses reflection to find the subcommand classes in the specified assemblies. To use [source generation](SourceGeneration.md#generating-a-command-manager) to find the commands at compile time and provide that information to a generated command manager, you must define a class as -follows, using the `GeneratedCommandManagerAttribute`: +follows, using the [`GeneratedCommandManagerAttribute`][]: ```csharp [GeneratedCommandManager] @@ -391,7 +391,7 @@ public static async Task Main() ``` In this case, if you want to use commands from other assemblies, you must specify them using the -`GeneratedCommandManagerAttribute`, and they can only come from assemblies that are directly +[`GeneratedCommandManagerAttribute`][], and they can only come from assemblies that are directly referenced from your application, not dynamically loaded ones. ### Subcommand options @@ -426,7 +426,7 @@ public static int Main() ``` This code makes command names case sensitive by using the invariant string comparer (the default is -`StringComparison.OrdinalIgnoreCase`, which is case insensitive), enables a name transformation, +[`StringComparison.OrdinalIgnoreCase`][], which is case insensitive), enables a name transformation, and also sets some [usage help options](#subcommand-usage-help). ### Custom error handling @@ -587,7 +587,7 @@ is enabled, none of the commands use custom parsing, and all commands use the sa argument name transformation, and argument name prefixes. You can force or disable the inclusion of the command help instruction by using the -`UsageWriter.IncludeCommandHelpInstruction` property. +[`UsageWriter.IncludeCommandHelpInstruction`][] property. Other properties let you configure indentation and colors, among others. @@ -614,14 +614,14 @@ description of the command can be customized using the [`LocalizedStringProvider ## Nested subcommands -Ookii.CommandLine supports nested subcommands through the `ParentCommandAttribute`, the -`ParentCommand` class, and the `CommandOptions.ParentCommand` property. The [`CommandManager`][] -will only return commands whose `ParentCommandAttribute` matches the type specified in the -`CommandOptions.ParentCommand` property. By default, this property is null, so commands that do not +Ookii.CommandLine supports nested subcommands through the [`ParentCommandAttribute`][], the +[`ParentCommand`][] class, and the [`CommandOptions.ParentCommand`][] property. The [`CommandManager`][] +will only return commands whose [`ParentCommandAttribute`][] matches the type specified in the +[`CommandOptions.ParentCommand`][] property. By default, this property is null, so commands that do not have a parent command will be returned. To create a command that can have nested commands, the easiest way is to create a class that derives -from the `ParentCommand` class. +from the [`ParentCommand`][] class. ```csharp [Command] @@ -631,13 +631,13 @@ class MyParentCommand : ParentCommand } ``` -> `ParentCommand` uses `ICommandWithCustomParsing`, so it cannot use the `GeneratedParserAttribute`. +> [`ParentCommand`][] uses [`ICommandWithCustomParsing`][], so it cannot use the [`GeneratedParserAttribute`][]. -Typically, this class can be empty, although `ParentCommand` provides several protected methods you +Typically, this class can be empty, although [`ParentCommand`][] provides several protected methods you can override to customize the behavior. To define a command that is nested under `MyParentCommand`, you need to use the -`ParentCommandAttribute`. +[`ParentCommandAttribute`][]. ```csharp [GeneratedParser] @@ -649,7 +649,7 @@ partial class ChildCommand : ICommand } ``` -When run, `MyParentCommand` will modify the `CommandOptions.ParentCommand` property and use the +When run, `MyParentCommand` will modify the [`CommandOptions.ParentCommand`][] property and use the [`CommandManager`][] again to find and execute the nested commands. Note that the automatic version command has no parent and will therefore only exist at the top @@ -661,47 +661,58 @@ functionality. The next page will discuss Ookii.CommandLine's [source generation](SourceGeneration.md) in more detail. -[`AliasAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_AliasAttribute.htm -[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm -[`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 -[`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.htm -[`CommandManager.GetCommand()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_CommandManager_GetCommand.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 -[`CommandNameTransform`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform.htm -[`CommandOptions.AutoVersionCommand`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandOptions_AutoVersionCommand.htm -[`CommandOptions.CommandFilter`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandFilter.htm -[`CommandOptions.CommandNameTransform`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform.htm -[`CommandOptions.StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm -[`CommandOptions`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Commands_CommandOptions.htm -[`CreateCommand()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm +[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AliasAttribute.htm +[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm +[`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm +[`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm +[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm +[`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandManager.GetCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_CommandManager_GetCommand.htm +[`CommandManager.ParseResult`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandManager_ParseResult.htm +[`CommandManager.RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm +[`CommandNameTransform`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform.htm +[`CommandOptions.AutoCommandPrefixAliases`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_AutoCommandPrefixAliases.htm +[`CommandOptions.AutoVersionCommand`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_AutoVersionCommand.htm +[`CommandOptions.CommandFilter`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandFilter.htm +[`CommandOptions.CommandNameTransform`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform.htm +[`CommandOptions.ParentCommand`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_ParentCommand.htm +[`CommandOptions.StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm +[`CommandOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandOptions.htm +[`CreateCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand.htm [`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute [`Environment.GetCommandLineArgs()`]: https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs -[`IAsyncCommand.RunAsync()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync.htm -[`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 -[`IncludeApplicationDescriptionBeforeCommandList`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList.htm -[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_LocalizedStringProvider.htm -[`NameTransform.DashCase`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_NameTransform.htm -[`NameTransform.None`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_NameTransform.htm -[`Ookii.CommandLine.Commands`]: https://www.ookii.org/docs/commandline-3.1/html/N_Ookii_CommandLine_Commands.htm -[`ParseOptions.AutoVersionArgument`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseOptions_AutoVersionArgument.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 -[`RunCommand()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm -[`RunCommand`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm -[`RunCommandAsync()`]: https://www.ookii.org/docs/commandline-3.1/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm -[`StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm -[`UsageWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_UsageWriter.htm -[`WriteCommandDescription()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteCommandDescription.htm -[`WriteCommandHelpInstruction()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteCommandHelpInstruction.htm -[`WriteCommandListUsageCore()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteCommandListUsageCore.htm -[`WriteCommandListUsageSyntax()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_UsageWriter_WriteCommandListUsageSyntax.htm -[RunAsync()_0]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_RunAsync.htm -[`CommandManager.ParseResult`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_Commands_CommandManager_ParseResult.htm -[`ParseResult.Status`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_ParseResult_Status.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 +[`IAsyncCommand.RunAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync.htm +[`IAsyncCommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_IAsyncCommand.htm +[`ICommand.Run()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_ICommand_Run.htm +[`ICommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommand.htm +[`ICommandWithCustomParsing.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_ICommandWithCustomParsing_Parse.htm +[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm +[`IncludeApplicationDescriptionBeforeCommandList`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescriptionBeforeCommandList.htm +[`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm +[`NameTransform.DashCase`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_NameTransform.htm +[`NameTransform.None`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_NameTransform.htm +[`Ookii.CommandLine.Commands`]: https://www.ookii.org/docs/commandline-4.0/html/N_Ookii_CommandLine_Commands.htm +[`ParentCommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ParentCommand.htm +[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm +[`ParseOptions.AutoVersionArgument`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_AutoVersionArgument.htm +[`ParseOptions`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptions.htm +[`ParseOptionsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParseOptionsAttribute.htm +[`ParseResult.RemainingArguments`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_RemainingArguments.htm +[`ParseResult.Status`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseResult_Status.htm +[`RunCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm +[`RunCommand`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand.htm +[`RunCommandAsync()`]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync.htm +[`StringComparison.OrdinalIgnoreCase`]: https://learn.microsoft.com/dotnet/api/system.stringcomparison +[`StripCommandNameSuffix`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix.htm +[`UsageWriter.IncludeCommandHelpInstruction`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeCommandHelpInstruction.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm +[`WriteCommandDescription()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteCommandDescription.htm +[`WriteCommandHelpInstruction()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteCommandHelpInstruction.htm +[`WriteCommandListUsageCore()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteCommandListUsageCore.htm +[`WriteCommandListUsageSyntax()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_UsageWriter_WriteCommandListUsageSyntax.htm +[RunAsync()_0]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_RunAsync.htm diff --git a/docs/refs.json b/docs/refs.json index ba548eed..4e3985a9 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -78,10 +78,12 @@ "CommandNameComparison": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameComparison", "CommandNameTransform": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform", "CommandOptions": "T_Ookii_CommandLine_Commands_CommandOptions", + "CommandOptions.AutoCommandPrefixAliases": "P_Ookii_CommandLine_Commands_CommandOptions_AutoCommandPrefixAliases", "CommandOptions.AutoVersionCommand": "P_Ookii_CommandLine_Commands_CommandOptions_AutoVersionCommand", "CommandOptions.CommandFilter": "P_Ookii_CommandLine_Commands_CommandOptions_CommandFilter", "CommandOptions.CommandNameTransform": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform", "CommandOptions.IsPosix": "P_Ookii_CommandLine_Commands_CommandOptions_IsPosix", + "CommandOptions.ParentCommand": "P_Ookii_CommandLine_Commands_CommandOptions_ParentCommand", "CommandOptions.StripCommandNameSuffix": "P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix", "Console.WindowWidth": "#system.console.windowwidth", "CreateCommand()": "Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand", @@ -92,6 +94,7 @@ "CultureInfo": "#system.globalization.cultureinfo", "CultureInfo.InvariantCulture": "#system.globalization.cultureinfo.invariantculture", "CurrentCulture": "#system.globalization.cultureinfo.currentculture", + "DatabaseCommand": null, "DateOnly": "#system.dateonly", "DateTime": "#system.datetime", "DayOfWeek": "#system.dayofweek", @@ -101,6 +104,7 @@ "P_Ookii_CommandLine_CommandLineArgument_DefaultValue", "P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue" ], + "DeleteCommand": null, "DescriptionAttribute": "#system.componentmodel.descriptionattribute", "DescriptionListFilterMode.Information": "T_Ookii_CommandLine_DescriptionListFilterMode", "Dictionary": "#system.collections.generic.dictionary-2", @@ -180,6 +184,7 @@ "LineWrappingTextWriter.Wrapping": "P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping", "List": "#system.collections.generic.list-1", "List": "#system.collections.generic.list-1", + "LoadPlugins()": null, "LocalizedStringProvider": "T_Ookii_CommandLine_LocalizedStringProvider", "Main()": null, "MaxLines": null, @@ -196,6 +201,7 @@ "P_Ookii_CommandLine_Validation_ValidateStringLengthAttribute_Mode" ], "MultiValueSeparatorAttribute": "T_Ookii_CommandLine_MultiValueSeparatorAttribute", + "MyParentCommand": null, "NameTransform.DashCase": "T_Ookii_CommandLine_NameTransform", "NameTransform.None": "T_Ookii_CommandLine_NameTransform", "Nullable": "#system.nullable-1", @@ -207,6 +213,8 @@ "Ookii.CommandLine.Commands": "N_Ookii_CommandLine_Commands", "Ookii.CommandLine.Terminal": "N_Ookii_CommandLine_Terminal", "Ookii.CommandLine.Validation": "N_Ookii_CommandLine_Validation", + "ParentCommand": "T_Ookii_CommandLine_Commands_ParentCommand", + "ParentCommandAttribute": "T_Ookii_CommandLine_Commands_ParentCommandAttribute", "Parse()": [ "M_Ookii_CommandLine_CommandLineParser_1_Parse_1", "M_Ookii_CommandLine_CommandLineParser_1_Parse", @@ -262,6 +270,7 @@ ], "ProhibitsAttribute": "T_Ookii_CommandLine_Validation_ProhibitsAttribute", "ReadCommand": null, + "ReadDirectoryCommand": null, "ReadOnlyCollection": "#system.collections.objectmodel.readonlycollection-1", "ReadOnlyMemory": "#system.readonlymemory-1", "ReadOnlySpan": "#system.readonlyspan-1", @@ -273,12 +282,14 @@ "M_Ookii_CommandLine_Commands_AsyncCommandBase_Run", "M_Ookii_CommandLine_Commands_ICommand_Run" ], + "Run(Async)": null, "RunAsync()": [ "M_Ookii_CommandLine_Commands_AsyncCommandBase_RunAsync", "M_Ookii_CommandLine_Commands_IAsyncCommand_RunAsync" ], "RunCommand": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand", "RunCommand()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand", + "RunCommand(Async)": null, "RunCommandAsync()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync", "SetConsoleMode": "https://learn.microsoft.com/windows/console/setconsolemode", "ShortAliasAttribute": "T_Ookii_CommandLine_ShortAliasAttribute", @@ -287,6 +298,7 @@ "StreamReader": "#system.io.streamreader", "String": "#system.string", "StringComparison": "#system.stringcomparison", + "StringComparison.OrdinalIgnoreCase": "#system.stringcomparison", "StringWriter": "#system.io.stringwriter", "StripCommandNameSuffix": "P_Ookii_CommandLine_Commands_CommandOptions_StripCommandNameSuffix", "System.ComponentModel.DescriptionAttribute": "#system.componentmodel.descriptionattribute", @@ -305,6 +317,7 @@ "UsageWriter.ArgumentDescriptionListFilter": "P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListFilter", "UsageWriter.ArgumentDescriptionListOrder": "P_Ookii_CommandLine_UsageWriter_ArgumentDescriptionListOrder", "UsageWriter.IncludeApplicationDescription": "P_Ookii_CommandLine_UsageWriter_IncludeApplicationDescription", + "UsageWriter.IncludeCommandHelpInstruction": "P_Ookii_CommandLine_UsageWriter_IncludeCommandHelpInstruction", "UsageWriter.IncludeValidatorsInDescription": "P_Ookii_CommandLine_UsageWriter_IncludeValidatorsInDescription", "UsageWriter.UseAbbreviatedSyntax": "P_Ookii_CommandLine_UsageWriter_UseAbbreviatedSyntax", "UsageWriter.UseColor": "P_Ookii_CommandLine_UsageWriter_UseColor", From fa246b55effd7d21211eeab44a6b2b4b8a0f6e45 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 12:00:46 -0700 Subject: [PATCH 164/234] Add API links to source generation docs. --- docs/SourceGeneration.md | 99 ++++++++++++++++++++++++---------------- docs/refs.json | 10 ++++ 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/docs/SourceGeneration.md b/docs/SourceGeneration.md index 53385787..204ddf1e 100644 --- a/docs/SourceGeneration.md +++ b/docs/SourceGeneration.md @@ -3,16 +3,16 @@ Ookii.CommandLine has two ways by which it can determine which arguments are available. - Reflection will inspect the members of the arguments type at runtime, check for the - `CommandLineParserAttribute`, and provide that information to the `CommandLineParser`. This was - the only method available before version 4.0, and is still used if the `GeneratedParserAttribute` + [`CommandLineArgumentAttribute`][], and provide that information to the [`CommandLineParser`][]. This was + the only method available before version 4.0, and is still used if the [`GeneratedParserAttribute`][] is not present. - Source generation will perform the same inspection at compile time, generating C# code that will - provide the required information to the `CommandLineParser` with less runtime overhead. This is - used as of version 4.0 when the `GeneratedParserAttribute` is present. + provide the required information to the [`CommandLineParser`][] with less runtime overhead. This is + used as of version 4.0 when the [`GeneratedParserAttribute`][] is present. -The same also applies to [subcommands](Subcommands.md). The `CommandManager` class uses runtime +The same also applies to [subcommands](Subcommands.md). The [`CommandManager`][] class uses runtime reflection by default to discover the subcommands in an assembly, and source generation is available -with the `GeneratedCommandManagerAttribute` to do that same work at compile time. +with the [`GeneratedCommandManagerAttribute`][] to do that same work at compile time. Using source generation has several benefits: @@ -24,7 +24,7 @@ Using source generation has several benefits: [trimmed](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options). It's not possible to statically determine what types are needed to determine arguments using reflection, so trimming is not possible at all with reflection. -- Improved performance; benchmarks show that instantiating a `CommandLineParser` using a +- Improved performance; benchmarks show that instantiating a [`CommandLineParser`][] using a generated parser is up to thirty times faster than using reflection. A few restrictions apply to projects that use Ookii.CommandLine's source generation: @@ -34,8 +34,8 @@ A few restrictions apply to projects that use Ookii.CommandLine's source generat - The project must be built using using the .Net 6.0 SDK or a later version. - You can still target older runtimes supported by Ookii.CommandLine, down to .Net Framework 4.6, but you must build the project using the .Net 6.0 SDK or newer. -- If you use the `ArgumentConverterAttribute` or `ParentCommandAttribute`, you must use the - constructor that takes a `Type` instance. The constructor that takes a string is not supported. +- If you use the [`ArgumentConverterAttribute`][] or [`ParentCommandAttribute`][], you must use the + constructor that takes a [`Type`][] instance. The constructor that takes a string is not supported. - The generated arguments or command manager class may not be nested in another type. - The generated arguments or command manager class may not have generic type parameters. @@ -44,7 +44,7 @@ Generally, it's recommended to use source generation unless you cannot meet thes ## Generating a parser To use source generation to determine the command line arguments defined by a class, apply the -`GeneratedParserAttribute` attribute to that class. You must also mark the class as `partial`, +[`GeneratedParserAttribute`][] attribute to that class. You must also mark the class as `partial`, because the source generator will add additional members to your class. ```csharp @@ -57,19 +57,19 @@ partial class Arguments ``` The source generator will inspect the members and attributes of the class, and generate C# code -that provides that information to a `CommandLineParser`, without needing to use reflection. While +that provides that information to a [`CommandLineParser`][], without needing to use reflection. While doing so, it checks whether your class violates any rules for defining arguments, and [emits warnings and errors](SourceGenerationDiagnostics.md) if it does. -If any of the arguments has a type for which there is no built-in `ArgumentConverter` class, and -the argument doesn't use the `ArgumentConverterAttribute`, the source generator will check whether +If any of the arguments has a type for which there is no built-in [`ArgumentConverter`][] class, and +the argument doesn't use the [`ArgumentConverterAttribute`][], the source generator will check whether the type supports any of the standard methods of [argument value conversion](Arguments.md#argument-value-conversion), -and if it does, it will generate an `ArgumentConverter` implementation for that type, and uses it +and if it does, it will generate an [`ArgumentConverter`][] implementation for that type, and uses it for the argument. -Generated `ArgumentConverter` classes are internal to your project, and placed in the +Generated [`ArgumentConverter`][] classes are internal to your project, and placed in the `Ookii.CommandLine.Conversion.Generated` namespace. The namespace can be customized using the -`GeneratedConverterNamespaceAttribute` attribute. +[`GeneratedConverterNamespaceAttribute`][] attribute. If you use Visual Studio, you can view the generated files by looking under Dependencies, Analyzers, Ookii.CommandLine.Generator in the Solution Explorer. @@ -79,8 +79,8 @@ case the generated files will be placed under the `obj` folder of your project. ### Using a generated parser -You can use the regular `CommandLineParser` or `CommandLineParser` constructors, or the static -`CommandLineParser.Parse()` methods, which will automatically use the generated argument +You can use the regular [`CommandLineParser`][] or [`CommandLineParser`][] constructors, or the static +[`CommandLineParser.Parse()`][] methods, which will automatically use the generated argument information if it is available. For convenience, the source generator also adds the following methods to your arguments class (where @@ -96,11 +96,11 @@ public static Arguments? Parse(string[] args, ParseOptions? options = null); public static Arguments? Parse(ReadOnlyMemory args, ParseOptions? options = null); ``` -Use the `CreateParser()` method as an alternative to the `CommandLineParser` constructor, and the -`Parse()` methods as an alternative to the static `CommandLineParser.Parse()` methods. +Use the [`CreateParser()`][CreateParser()_1] method as an alternative to the [`CommandLineParser`][] constructor, and the +[`Parse()`][Parse()_7] methods as an alternative to the static [`CommandLineParser.Parse()`][] methods. Generally, it's recommended to use these generated methods. If you want to trim your application, -you must use them, since the regular `CommandLineParser` constructor will still use reflection to +you must use them, since the regular [`CommandLineParser`][] constructor will still use reflection to determine if generated argument information is present, and therefore still prohibits trimming. So, if you had the following code before using source generation: @@ -118,16 +118,16 @@ var arguments = Arguments.Parse(); Everything else remains the same. If your project targets .Net 7.0 or later, the generated class will implement the -`IParserProvider` and `IParser` interfaces, which define the generated methods. +[`IParserProvider`][] and [`IParser`][] interfaces, which define the generated methods. -Generating the `Parse()` methods is optional, and can be disabled using the -`GeneratedParserAttribute.GenerateParseMethods` property. The `CreateParser()` method is always +Generating the [`Parse()`][Parse()_7] methods is optional, and can be disabled using the +[`GeneratedParserAttribute.GenerateParseMethods`][] property. The [`CreateParser()`][CreateParser()_1] method is always generated. ### Automatic ordering of positional arguments -When using the `GeneratedParserAttribute`, you do not have to specify explicit positions for -positional arguments. Instead, you can use the `CommandLineArgumentAttribute.IsPositional` +When using the [`GeneratedParserAttribute`][], you do not have to specify explicit positions for +positional arguments. Instead, you can use the [`CommandLineArgumentAttribute.IsPositional`][] property to indicate which arguments are positional, and the order will be determined by the order of the members that define the arguments. @@ -190,12 +190,12 @@ When using a reflection-based parser, `Arg2` would have its value set to "foo" w Ookii.CommandLine doesn't assign the property if the argument is not specifies), but that default value would not be included in the usage help, whereas the default value of `Arg1` will be. -With the `GeneratedParserAttribute`, both `Arg1` and `Arg2` will have the default value of "foo" +With the [`GeneratedParserAttribute`][], both `Arg1` and `Arg2` will have the default value of "foo" shown in the usage help, making the two forms identical. Additionally, `Arg2` could be marked non-nullable because it was initialized to a non-null value, something which isn't possible for `Arg1` without initializing the property to a value that will not be used. -If both a property initializer and the `DefaultValue` property are both used, the `DefaultValue` +If both a property initializer and the [`DefaultValue`][DefaultValue_1] property are both used, the [`DefaultValue`][DefaultValue_1] property takes precedence. This only works if the property initializer is a literal, enumeration value, reference to a constant, @@ -209,18 +209,18 @@ If a different kind of expression is used in the property initializer, such as a ## Generating a command manager -You can apply the `GeneratedParserAttribute` to a command, and generate the parser for that command -at compile time. This will work with the `CommandManager` class without further changes to your +You can apply the [`GeneratedParserAttribute`][] to a command, and generate the parser for that command +at compile time. This will work with the [`CommandManager`][] class without further changes to your code. -The `GeneratedParserAttribute` works the same for command classes as it does for any other arguments -class, with one exception: the static `Parse()` methods are not generated by default for command -classes. You must explicitly set the `GeneratedParserAttribute.GenerateParseMethods` to `true` if +The [`GeneratedParserAttribute`][] works the same for command classes as it does for any other arguments +class, with one exception: the static [`Parse()`][Parse()_7] methods are not generated by default for command +classes. You must explicitly set the [`GeneratedParserAttribute.GenerateParseMethods`][] to `true` if you want them to be generated. -However, the `CommandManager` class still uses reflection to determine what commands are available +However, the [`CommandManager`][] class still uses reflection to determine what commands are available in the assembly or assemblies you specify. To determine the available commands at compile time, you -must define a partial class with the `GeneratedCommandManagerAttribute`: +must define a partial class with the [`GeneratedCommandManagerAttribute`][]: ```csharp [GeneratedCommandManager] @@ -233,7 +233,7 @@ The source generator will find all command classes in your project, and generate those command to the generated command manager without needing reflection. If you need to load commands from a different assembly, or multiple assemblies, you can use the -`GeneratedCommandManagerAttribute.AssemblyNames` property. This property can use either just the +[`GeneratedCommandManagerAttribute.AssemblyNames`][] property. This property can use either just the name of the assembly, or the full assembly identity including version, culture and public key token. @@ -250,15 +250,15 @@ reflection. ### Using a generated command manager -The source generator will add `CommandManager` as a base class to your class, and add the +The source generator will add [`CommandManager`][] as a base class to your class, and add the following constructor to the class: ```csharp public GeneratedManager(CommandOptions? options = null) ``` -This means a class with the `GeneratedCommandManagerAttribute` can be used as a drop-in replacement -of the regular `CommandManager` class. +This means a class with the [`GeneratedCommandManagerAttribute`][] can be used as a drop-in replacement +of the regular [`CommandManager`][] class. If you had the following code before using source generation: @@ -276,3 +276,24 @@ return manager.RunCommand() ?? 1; Next, we will take a look at several [utility classes](Utilities.md) provided, and used, by Ookii.CommandLine. + +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm +[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm +[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm +[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser_1.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm +[`GeneratedCommandManagerAttribute.AssemblyNames`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute_AssemblyNames.htm +[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm +[`GeneratedConverterNamespaceAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_GeneratedConverterNamespaceAttribute.htm +[`GeneratedParserAttribute.GenerateParseMethods`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_GeneratedParserAttribute_GenerateParseMethods.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`IParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_IParser_1.htm +[`IParserProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_IParserProvider_1.htm +[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm +[`Type`]: https://learn.microsoft.com/dotnet/api/system.type +[CreateParser()_1]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_IParserProvider_1_CreateParser.htm +[DefaultValue_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[Parse()_7]: https://www.ookii.org/docs/commandline-4.0/html/Overload_Ookii_CommandLine_IParser_1_Parse.htm diff --git a/docs/refs.json b/docs/refs.json index 4e3985a9..2075fbee 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -6,6 +6,8 @@ "AliasAttribute": "T_Ookii_CommandLine_AliasAttribute", "AllowDuplicateDictionaryKeysAttribute": "T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute", "ApplicationFriendlyNameAttribute": "T_Ookii_CommandLine_ApplicationFriendlyNameAttribute", + "Arg1": null, + "Arg2": null, "ArgumentConverter": "T_Ookii_CommandLine_Conversion_ArgumentConverter", "ArgumentConverterAttribute": "T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute", "ArgumentNameComparison": [ @@ -99,6 +101,7 @@ "DateTime": "#system.datetime", "DayOfWeek": "#system.dayofweek", "DayOfWeek.Monday": "#system.dayofweek", + "DayOfWeek.Tuesday": "#system.dayofweek", "DayOfWeek.Wednesday": "#system.dayofweek", "DefaultValue": [ "P_Ookii_CommandLine_CommandLineArgument_DefaultValue", @@ -134,7 +137,10 @@ ], "Foo": null, "GeneratedCommandManagerAttribute": "T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute", + "GeneratedCommandManagerAttribute.AssemblyNames": "P_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute_AssemblyNames", + "GeneratedConverterNamespaceAttribute": "T_Ookii_CommandLine_Conversion_GeneratedConverterNamespaceAttribute", "GeneratedParserAttribute": "T_Ookii_CommandLine_GeneratedParserAttribute", + "GeneratedParserAttribute.GenerateParseMethods": "P_Ookii_CommandLine_GeneratedParserAttribute_GenerateParseMethods", "GetArgument": "M_Ookii_CommandLine_CommandLineParser_GetArgument", "GetCommand()": "M_Ookii_CommandLine_Commands_CommandManager_GetCommand", "GetErrorMessage()": "M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetErrorMessage", @@ -164,6 +170,8 @@ "Int32": "#system.int32", "Inverted": null, "IParsable": "#system.iparsable-1", + "IParser": "T_Ookii_CommandLine_IParser_1", + "IParserProvider": "T_Ookii_CommandLine_IParserProvider_1", "ISpanParsable": "#system.ispanparsable-1", "IsPositional": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional", "IsPosix": [ @@ -211,6 +219,7 @@ "M_Ookii_CommandLine_LocalizedStringProvider_NullArgumentValue" ], "Ookii.CommandLine.Commands": "N_Ookii_CommandLine_Commands", + "Ookii.CommandLine.Conversion.Generated": null, "Ookii.CommandLine.Terminal": "N_Ookii_CommandLine_Terminal", "Ookii.CommandLine.Validation": "N_Ookii_CommandLine_Validation", "ParentCommand": "T_Ookii_CommandLine_Commands_ParentCommand", @@ -306,6 +315,7 @@ "TextFormat": "T_Ookii_CommandLine_Terminal_TextFormat", "TextWriter": "#system.io.textwriter", "ToString()": "#system.object.tostring", + "Type": "#system.type", "TypeConverter": "#system.componentmodel.typeconverter", "TypeConverterArgumentConverter": "T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter", "TypeConverterArgumentConverter": "T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter_1", From 21a60f6bcb5cefedcbbc654f37d57ca5f0981b49 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 12:03:55 -0700 Subject: [PATCH 165/234] Update utilities docs including API links. --- docs/Utilities.md | 77 +++++++++++------------------------------------ 1 file changed, 18 insertions(+), 59 deletions(-) diff --git a/docs/Utilities.md b/docs/Utilities.md index 811338d7..1f8d416d 100644 --- a/docs/Utilities.md +++ b/docs/Utilities.md @@ -1,8 +1,8 @@ # Utility types Ookii.CommandLine comes with a few utilities that it uses internally, but which may be of use to -anyone writing console applications. These are the [`LineWrappingTextWriter`][] class, virtual -terminal support, and the [`TypeConverterBase`][] class. +anyone writing console applications. These are the [`LineWrappingTextWriter`][] class and virtual +terminal support. ## LineWrappingTextWriter @@ -128,62 +128,21 @@ and they return a disposable type that will revert the console mode when dispose collected. On other platforms, it only checks for support and disposing the returned instance does nothing. -## TypeConverterBase\ - -If a type does not have a suitable default [`TypeConverter`][], `Parse()` method or constructor, or if -you want to use a custom conversion that's different than the default, Ookii.CommandLine requires -you to create a [`TypeConverter`][] that can convert from a string. To make this process easier, the -[`TypeConverterBase`][] class is provided. - -This class implements the [`CanConvertFrom()`][] and [`ConvertFrom()`][] method for you to check if the source -type is a string, and provides strongly typed conversion methods that you can implement. - -For example, the following is a custom type converter for booleans that accepts "yes", "no", "1" and -"0" in addition to the regular "true" and "false" values. - -```csharp -class YesNoConverter : TypeConverterBase -{ - protected override bool Convert(ITypeDescriptorContext? context, CultureInfo? culture, string value) - { - return value.ToLower(culture) switch - { - "yes" or "1" => true, - "no" or "0" => false, - _ => bool.Parse(value), - }; - } -} -``` - -You can then use this converter as the custom converter for a boolean (switch) argument using the -[`TypeConverterAttribute`][]. - -If you want to customize the conversion to string, you can do this too (it uses [`ToString()`][] by -default), but Ookii.CommandLine never uses this, so it's only relevant if you want to use the -converter in other contexts. - -[`CanConvertFrom()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_TypeConverterBase_1_CanConvertFrom.htm [`Console.WindowWidth`]: https://learn.microsoft.com/dotnet/api/system.console.windowwidth -[`ConvertFrom()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_TypeConverterBase_1_ConvertFrom.htm -[`EnableColor()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_EnableColor.htm -[`EnableVirtualTerminalSequences()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_EnableVirtualTerminalSequences.htm -[`Indent`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_LineWrappingTextWriter_Indent.htm -[`LineWrappingTextWriter.ForConsoleError()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleError.htm -[`LineWrappingTextWriter.ForConsoleOut()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleOut.htm -[`LineWrappingTextWriter.Indent`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_LineWrappingTextWriter_Indent.htm -[`LineWrappingTextWriter.ResetIndent()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent.htm -[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm -[`Ookii.CommandLine.Terminal`]: https://www.ookii.org/docs/commandline-3.1/html/N_Ookii_CommandLine_Terminal.htm -[`ResetIndent()`]: https://www.ookii.org/docs/commandline-3.1/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent.htm -[`TextFormat`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Terminal_TextFormat.htm +[`EnableColor()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_EnableColor.htm +[`EnableVirtualTerminalSequences()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_EnableVirtualTerminalSequences.htm +[`Indent`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_LineWrappingTextWriter_Indent.htm +[`LineWrappingTextWriter.ForConsoleError()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleError.htm +[`LineWrappingTextWriter.ForConsoleOut()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleOut.htm +[`LineWrappingTextWriter.Indent`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_LineWrappingTextWriter_Indent.htm +[`LineWrappingTextWriter.ResetIndent()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent.htm +[`LineWrappingTextWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LineWrappingTextWriter.htm +[`Ookii.CommandLine.Terminal`]: https://www.ookii.org/docs/commandline-4.0/html/N_Ookii_CommandLine_Terminal.htm +[`ResetIndent()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_LineWrappingTextWriter_ResetIndent.htm +[`TextFormat`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Terminal_TextFormat.htm [`TextWriter`]: https://learn.microsoft.com/dotnet/api/system.io.textwriter -[`ToString()`]: https://learn.microsoft.com/dotnet/api/system.object.tostring -[`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 -[`UsageWriter`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_UsageWriter.htm -[`VirtualTerminal`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_Terminal_VirtualTerminal.htm -[`LineWrappingTextWriter.Wrapping`]: https://www.ookii.org/docs/commandline-3.1/html/P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping.htm -[`WrappingMode.Disabled`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_WrappingMode.htm -[`WrappingMode.EnabledNoForce`]: https://www.ookii.org/docs/commandline-3.1/html/T_Ookii_CommandLine_WrappingMode.htm +[`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm +[`VirtualTerminal`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Terminal_VirtualTerminal.htm +[`LineWrappingTextWriter.Wrapping`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_LineWrappingTextWriter_Wrapping.htm +[`WrappingMode.Disabled`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_WrappingMode.htm +[`WrappingMode.EnabledNoForce`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_WrappingMode.htm From 45b0705befdd5e1f21da9ad8a20b104068ee3a26 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 12:15:19 -0700 Subject: [PATCH 166/234] Add API links to source generation diagnostics docs. --- docs/SourceGenerationDiagnostics.md | 213 +++++++++++++++++----------- docs/refs.json | 22 +++ 2 files changed, 153 insertions(+), 82 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 49ef0c56..cab8754e 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -8,15 +8,15 @@ does anything unsupported by Ookii.CommandLine. Among others, it checks for thin - Whether positional arguments have duplicate numbering. - Arguments with types that cannot be converted from a string. - Attribute or property combinations that are ignored. -- Using the `CommandLineArgument` with a private member, or a method with an incorrect signature. +- Using the [`CommandLineArgument`][] with a private member, or a method with an incorrect signature. Without source generation, these mistakes would either lead to a runtime exception when creating the -`CommandLineParser` class, or would be silently ignored. With source generation, you can instead +[`CommandLineParser`][] class, or would be silently ignored. With source generation, you can instead catch any problems during compile time, which reduces the risk of bugs. Not all errors can be caught at compile time. For example, the source generator does not check for -duplicate argument names, because the `ParseOptions.ArgumentNameTransform` and -`ParseOptions.ArgumentNameComparison` properties can render the result of this check inaccurate. +duplicate argument names, because the [`ParseOptions.ArgumentNameTransform`][] and +[`ParseOptions.ArgumentNameComparison`][] properties can render the result of this check inaccurate. ## Errors @@ -24,7 +24,7 @@ duplicate argument names, because the `ParseOptions.ArgumentNameTransform` and The command line arguments or command manager type must be a reference type. -A command line arguments type, or a type using the `GeneratedCommandManagerAttribute`, must be a +A command line arguments type, or a type using the [`GeneratedCommandManagerAttribute`][], must be a reference type, or class. Value types (or structures) cannot be used. For example, the following code triggers this error: @@ -42,7 +42,7 @@ partial struct Arguments // ERROR: The type must be a class. The command line arguments or command manager class must be partial. -When using the `GeneratedParserAttribute` or `GeneratedCommandManagerAttribute`, the target type +When using the [`GeneratedParserAttribute`][] or [`GeneratedCommandManagerAttribute`][], the target type must use the `partial` modifier. For example, the following code triggers this error: @@ -60,7 +60,7 @@ class Arguments // ERROR: The class must be partial The command line arguments or command manager class must not have any generic type arguments. -When using the `GeneratedParserAttribute` or `GeneratedCommandManagerAttribute`, the target type +When using the [`GeneratedParserAttribute`][] or [`GeneratedCommandManagerAttribute`][], the target type cannot be a generic type. For example, the following code triggers this error: @@ -78,7 +78,7 @@ partial class Arguments // ERROR: The class must not be generic The command line arguments or command manager class must not be nested in another type. -When using the `GeneratedParserAttribute` or `GeneratedCommandManagerAttribute`, the target type +When using the [`GeneratedParserAttribute`][] or [`GeneratedCommandManagerAttribute`][], the target type cannot be nested in another type. For example, the following code triggers this error: @@ -138,10 +138,10 @@ No command line argument converter exists for the argument's type. The argument uses a type (or in the case of a multi-value or dictionary argument, an element type) that cannot be converted from a string using the [default rules for argument conversion](Arguments.md#argument-value-conversion), -and no custom `ArgumentConverter` was specified. +and no custom [`ArgumentConverter`][] was specified. -To fix this error, either change the type of the argument, or create a custom `ArgumentConverter` -and use the `ArgumentConverterAttribute` on the argument. +To fix this error, either change the type of the argument, or create a custom [`ArgumentConverter`][] +and use the [`ArgumentConverterAttribute`][] on the argument. For example, the following code triggers this error: @@ -196,7 +196,7 @@ partial class Arguments ``` To fix this error, either use a regular `set` accessor, or if using .Net 7.0 or later, use the -`required` keyword (setting `IsRequired` is not necessary in this case): +`required` keyword (setting [`IsRequired`][IsRequired_1] is not necessary in this case): ```csharp [GeneratedParser] @@ -209,10 +209,10 @@ partial class Arguments ### OCL0010 -The `GeneratedParserAttribute` cannot be used with a class that implements the -`ICommandWithCustomParsing` interface. +The [`GeneratedParserAttribute`][] cannot be used with a class that implements the +[`ICommandWithCustomParsing`][] interface. -The `ICommandWIthCustomParsing` interface is for commands that do not use the `CommandLineParser` +The [`ICommandWithCustomParsing`][] interface is for commands that do not use the [`CommandLineParser`][] class, so a generated parser would not be used. For example, the following code triggers this error: @@ -281,7 +281,7 @@ partial class Arguments ### OCL0013 -One of the assembly names specified in the `GeneratedCommandManagerAttribute.AssemblyNames` property +One of the assembly names specified in the [`GeneratedCommandManagerAttribute.AssemblyNames`][] property is not valid. This error is used when you give the full assembly identify, but it cannot be parsed. For example, the following code triggers this error: @@ -296,11 +296,11 @@ partial class MyCommandManager ### OCL0014 -One of the assembly names specified in the `GeneratedCommandManagerAttribute.AssemblyNames` property +One of the assembly names specified in the [`GeneratedCommandManagerAttribute.AssemblyNames`][] property could not be resolved. Make sure it's an assembly that is referenced by the current project. If you wish to load commands from an assembly that is not directly referenced by your project, you -must use the regular `CommandManager` class, using reflection instead of source generation, instead. +must use the regular [`CommandManager`][] class, using reflection instead of source generation, instead. For example, the following code triggers this error: @@ -314,10 +314,10 @@ partial class MyCommandManager ### OCL0015 -The `ArgumentConverterAttribute` or `ParentCommandAttribute` must use the `typeof` keyword. +The [`ArgumentConverterAttribute`][] or [`ParentCommandAttribute`][] must use the `typeof` keyword. -The `ArgumentConverterAttribute` and `ParentCommandAttribute` have two constructors; one that takes -the `Type` of a converter or parent command, and one that takes the name of a type as a string. The +The [`ArgumentConverterAttribute`][] and [`ParentCommandAttribute`][] have two constructors; one that takes +the [`Type`][] of a converter or parent command, and one that takes the name of a type as a string. The string constructor is not supported when using source generation. For example, the following code triggers this error: @@ -332,18 +332,18 @@ partial class Arguments } ``` -To fix this error, either use the constructor that takes a `Type` using the `typeof` keyword, or +To fix this error, either use the constructor that takes a [`Type`][] using the `typeof` keyword, or do not use source generation. ### OCL0031 The argument does not have a long name or a short name. This happens when both the -`CommandLineArgumentAttribute.IsLong` and `CommandLineArgumentAttribute.IsShort` properties are set +[`CommandLineArgumentAttribute.IsLong`][] and [`CommandLineArgumentAttribute.IsShort`][] properties are set to false. This means that when using [long/short mode](Arguments.md#longshort-mode), the argument would not be usable. This error will be triggered regardless of the parsing mode you actually use, since that can be -changed at runtime by the `ParseOptions.Mode` property and is therefore not known at compile time. +changed at runtime by the [`ParseOptions.Mode`][] property and is therefore not known at compile time. For example, the following code triggers this error: @@ -359,8 +359,8 @@ partial class Arguments ### OCL0037 -Source generation with the `GeneratedParserAttribute` or `CommandManagerAttribute` requires at -least C# language version 8.0. +Source generation with the [`GeneratedParserAttribute`][] or [`GeneratedCommandManagerAttribute`][] requires +at least C# language version 8.0. The code that is generated by Ookii.CommandLine's [source generation](SourceGeneration.md) uses language features that are only available in C# 8.0. Use the `` configuration property @@ -373,15 +373,16 @@ version by default. ``` -If you cannot change the language version, remove the `GeneratedParserAttribute` or -`CommandManagerAttribute` and use the `CommandLineParser` class, `CommandLineParser.Parse()` -methods, or `CommandManager` class directly to use reflection instead of source generation. +If you cannot change the language version, remove the [`GeneratedParserAttribute`][] or +[`GeneratedCommandManagerAttribute`][] and use the [`CommandLineParser`][] class, +[`CommandLineParser.Parse()`][] methods, or [`CommandManager`][] class directly to use reflection +instead of source generation. ### OCL0038 -Positional arguments using an explicit position with the `CommandLineArgumentAttribute.Position` +Positional arguments using an explicit position with the [`CommandLineArgumentAttribute.Position`][] property, and those using a position derived from their member ordering using the -`CommandLineArgumentAttribute.IsPositional` property cannot be mixed. Note that this includes any +[`CommandLineArgumentAttribute.IsPositional`][] property cannot be mixed. Note that this includes any arguments defined in a base class. For example, the following code triggers this error: @@ -401,8 +402,8 @@ partial class Arguments Please switch all arguments to use either explicit or automatic positions. -Note that using `CommandLineArgumentAttribute.IsPositional` without an explicit position does not -work without the `GeneratedParserAttribute`. +Note that using [`CommandLineArgumentAttribute.IsPositional`][] without an explicit position does not +work without the [`GeneratedParserAttribute`][]. ## Warnings @@ -414,8 +415,8 @@ The arguments class itself, or one of the members defining an argument, has an a not used by Ookii.CommandLine. For example, the following code triggers this warning, because the current version of -Ookii.CommandLine no longer uses the `TypeConverterAttribute`, having replaced it with the -`ArgumentConverterAttribute`: +Ookii.CommandLine no longer uses the [`TypeConverterAttribute`][], having replaced it with the +[`ArgumentConverterAttribute`][]: ```csharp [GeneratedParser] @@ -434,7 +435,7 @@ other than Ookii.CommandLine, you should suppress or disable this warning. Methods that are not public and static will be ignored. -If the `CommandLineArgumentAttribute` is used on a method that is not a `public static` method, no +If the [`CommandLineArgumentAttribute`][] is used on a method that is not a `public static` method, no argument will be generated for this method. For example, the following code triggers this warning: @@ -453,7 +454,7 @@ partial class Arguments Properties that are not public instance properties will be ignored. -If the `CommandLineArgumentAttribute` is used on a property that is not a `public` property, or +If the [`CommandLineArgumentAttribute`][] is used on a property that is not a `public` property, or that is a `static` property, no argument will be generated for this property. For example, the following code triggers this warning: @@ -470,11 +471,11 @@ partial class Arguments ### OCL0019 -A command line arguments class has the `CommandAttribute` but does not implement the `ICommand` +A command line arguments class has the [`CommandAttribute`][] but does not implement the [`ICommand`][] interface. -Without the interface, the `CommandAttribute` is ignored and the class will not be treated as a -command by a regular or generated `CommandManager`. Both the `CommandAttribute` and the `ICommand` +Without the interface, the [`CommandAttribute`][] is ignored and the class will not be treated as a +command by a regular or generated [`CommandManager`][]. Both the [`CommandAttribute`][] and the [`ICommand`][] interface are required for commands. For example, the following code triggers this warning: @@ -489,7 +490,7 @@ partial class MyCommand // WARNING: The class doesn't implement ICommand } ``` -The inverse, implementing `ICommand` without using the `CommandAttribute`, does not generate a +The inverse, implementing [`ICommand`][] without using the [`CommandAttribute`][], does not generate a warning as this is a common pattern for subcommand base classes. ### OCL0020 @@ -516,11 +517,11 @@ partial class Arguments ### OCL0021 -The `CommandLineArgumentAttribute.IsRequired` property is ignored for a property with the +The [`CommandLineArgumentAttribute.IsRequired`][] property is ignored for a property with the `required` keyword. If the `required` keyword is present, the argument is required, even if you -set the `IsRequired` property to false explicitly. +set the [`IsRequired`][IsRequired_1] property to false explicitly. -> The `required` keyword is only available in .Net 7.0 and later; the `IsRequired` property should +> The `required` keyword is only available in .Net 7.0 and later; the [`IsRequired`][IsRequired_1] property should > be used to create required arguments in older versions of .Net. For example, the following code triggers this warning: @@ -558,14 +559,14 @@ partial class Arguments ### OCL0023 -The `ShortAliasAttribute` is ignored on an argument that does not have a short name. Set the -`CommandLineArgumentAttribute.IsShort` property to true set an explicit short name using the -`CommandLineArgumentAttribute.ShortName` property. Without a short name, any short aliases will not +The [`ShortAliasAttribute`][] is ignored on an argument that does not have a short name. Set the +[`CommandLineArgumentAttribute.IsShort`][] property to true set an explicit short name using the +[`CommandLineArgumentAttribute.ShortName`][] property. Without a short name, any short aliases will not be used. -Note that the `ShortAliasAttribute` is also ignored if `ParsingMode.LongShort` is not used, which is +Note that the [`ShortAliasAttribute`][] is also ignored if [`ParsingMode.LongShort`][] is not used, which is not checked by the source generator, because it can be changed at runtime using the -`ParseOptions.Mode` property. +[`ParseOptions.Mode`][] property. For example, the following code triggers this warning: @@ -583,12 +584,12 @@ partial class Arguments ### OLC0024 -The `AliasAttribute` is ignored on an argument with no long name. An argument has no long name only -if the `CommandLineArgumentAttribute.IsLong` property is set to false. +The [`AliasAttribute`][] is ignored on an argument with no long name. An argument has no long name only +if the [`CommandLineArgumentAttribute.IsLong`][] property is set to false. -Note that the `AliasAttribute` may still be used if `ParsingMode.LongShort` is not used, which is +Note that the [`AliasAttribute`][] may still be used if [`ParsingMode.LongShort`][] is not used, which is not checked by the source generator, because it can be changed at runtime using the -`ParseOptions.Mode` property. +[`ParseOptions.Mode`][] property. For example, the following code triggers this warning: @@ -606,11 +607,11 @@ partial class Arguments ### OCL0025 -The `CommandLineArgumentAttribute.IsHidden` property is ignored for positional arguments. +The [`CommandLineArgumentAttribute.IsHidden`][] property is ignored for positional arguments. Positional arguments cannot be hidden, because excluding them from the usage help would give incorrect positions for any additional positional arguments. A positional argument is therefore not -hidden even if `IsHidden` is set to true. +hidden even if [`IsHidden`][IsHidden_1] is set to true. For example, the following code triggers this warning: @@ -626,7 +627,7 @@ partial class Arguments ### OCL0026 -The namespace specified in the `GeneratedConverterNamespaceAttribute` is not a valid C# namespace +The namespace specified in the [`GeneratedConverterNamespaceAttribute`][] is not a valid C# namespace name, for example because one of the elements contains an unsupported character or starts with a digit. @@ -638,8 +639,8 @@ For example, the following code triggers this warning: ### OCL0027 -The `KeyConverterAttribute`, `ValueConverterAttribute`, `KeyValueSeparatorAttribute` and -`AllowDuplicateDictionaryKeysAttribute` attributes are only used for dictionary arguments, and will +The [`KeyConverterAttribute`][], [`ValueConverterAttribute`][], [`KeyValueSeparatorAttribute`][] and +[`AllowDuplicateDictionaryKeysAttribute`][] attributes are only used for dictionary arguments, and will be ignored if the argument is not a dictionary argument. For example, the following code triggers this warning: @@ -656,9 +657,9 @@ partial class Arguments ### OCL0028 -The `KeyConverterAttribute`, `ValueConverterAttribute`, and `KeyValueSeparatorAttribute` attributes -are used by the default `KeyValuePairConverter` for dictionary arguments, and will be ignored if the -argument uses the `ArgumentConverterAttribute` to specify a different converter. +The [`KeyConverterAttribute`][], [`ValueConverterAttribute`][], and [`KeyValueSeparatorAttribute`][] attributes +are used by the default [`KeyValuePairConverter`][] for dictionary arguments, and will be +ignored if the argument uses the [`ArgumentConverterAttribute`][] to specify a different converter. For example, the following code triggers this warning: @@ -675,7 +676,7 @@ partial class Arguments ### OCL0029 -The `MultiValueSeparatorAttribute` is only used for multi-value arguments (including dictionary +The [`MultiValueSeparatorAttribute`][] is only used for multi-value arguments (including dictionary arguments), and will be ignored if the argument is not a multi-value argument. For example, the following code triggers this warning: @@ -695,7 +696,7 @@ partial class Arguments An argument has an explicit name or short name starting with a number, which cannot be used with the '-' prefix. -If the `CommandLineParser` sees a dash followed by a digit, it will always interpret this as a +If the [`CommandLineParser`][] sees a dash followed by a digit, it will always interpret this as a value, because it may be a negative number. It is never interpreted as an argument name, even if the rest of the argument is not a valid number. @@ -712,16 +713,16 @@ partial class Arguments ``` This warning may be a false positive if you are using a different argument name prefix with the -`ParseOptionAttribute.ArgumentNamePrefixes` or `ParseOptions.ArgumentNamePrefixes` property, or if +[`ParseOptionsAttribute.ArgumentNamePrefixes`][] or [`ParseOptions.ArgumentNamePrefixes`][] property, or if you are using long/short mode and the name is a long name. In these cases, you should suppress or disable this warning. ### OCL0032 -The `CommandLineArgumentAttribute.IsShort` property is ignored if an explicit short name is set -using the `CommandLineArgumentAttribute.ShortName` property. +The [`CommandLineArgumentAttribute.IsShort`][] property is ignored if an explicit short name is set +using the [`CommandLineArgumentAttribute.ShortName`][] property. -If the `ShortName` property is set, it implies that `IsShort` is true, and manually setting it to +If the [`ShortName`][ShortName_1] property is set, it implies that [`IsShort`][] is true, and manually setting it to false will have no effect. For example, the following code triggers this warning: @@ -739,7 +740,7 @@ partial class Arguments ### OCL0033 -Arguments should have a description, set using the `DescriptionAttribute` attribute, for use in the +Arguments should have a description, set using the [`DescriptionAttribute`][] attribute, for use in the usage help. Arguments without a description are not guaranteed to be listed in the description list of the @@ -759,7 +760,7 @@ partial class Arguments ``` To fix this, write a concise description explaining the argument's purpose and usage, and apply the -`DescriptionAttribute` (or a derived attribute) to the member that defines the argument. +[`DescriptionAttribute`][] (or a derived attribute) to the member that defines the argument. ```csharp [GeneratedParser] @@ -772,11 +773,11 @@ partial class Arguments ``` This warning will not be emitted for arguments that are hidden using the -`CommandLineArgumentAttribute.IsHidden` property. +[`CommandLineArgumentAttribute.IsHidden`][] property. ### OCL0034 -Subcommands should have a description, set using the `DescriptionAttribute` attribute, for use in +Subcommands should have a description, set using the [`DescriptionAttribute`][] attribute, for use in the usage help. For example, the following code triggers this warning: @@ -794,7 +795,7 @@ partial class MyCommand : ICommand ``` To fix this, write a concise description explaining the command's purpose, and apply the -`DescriptionAttribute` (or a derived attribute) to the class that defines the command. +[`DescriptionAttribute`][] (or a derived attribute) to the class that defines the command. ```csharp [GeneratedParser] @@ -809,11 +810,11 @@ partial class MyCommand : ICommand ``` This warning will not be emitted for subcommands that are hidden using the -`CommandAttribute.IsHidden` property. +[`CommandAttribute.IsHidden`][] property. ### OCL0035 -The `ParentCommandAttribute` attribute is only used for subcommands, but was used on an arguments +The [`ParentCommandAttribute`][] attribute is only used for subcommands, but was used on an arguments type that isn't a subcommand. For example, the following code triggers this warning: @@ -832,10 +833,10 @@ partial class MyCommand ### OCL0036 -The `ApplicationFriendlyNameAttribute` attribute was used on a subcommand. The -`ApplicationFriendlyNameAttribute` is used by the automatic `-Version` argument, which is not +The [`ApplicationFriendlyNameAttribute`][] attribute was used on a subcommand. The +[`ApplicationFriendlyNameAttribute`][] is used by the automatic `-Version` argument, which is not created for subcommands, and the automatic `version` command only uses the -`ApplicationFriendlyNameAttribute` when applied to the entry assembly for the application. +[`ApplicationFriendlyNameAttribute`][] when applied to the entry assembly for the application. For example, the following code triggers this warning: @@ -890,14 +891,62 @@ partial class Arguments ``` This will not affect the actual value of the argument, since the property will not be set by the -`CommandLineParser` if the `CommandLineArgumentAttribute.DefaultValue` property is null. Therefore, +[`CommandLineParser`][] if the [`CommandLineArgumentAttribute.DefaultValue`][] property is null. Therefore, you can safely suppress this warning and include the relevant explanation of the default value in the property's description manually, if desired. To avoid this warning, use one of the supported expression types, or use the -`CommandLineArgumentAttribute.DefaultValue` property. This warning will not be emitted if the -`CommandLineArgumentAttribute.DefaultValue` property is not null, regardless of the initializer. +[`CommandLineArgumentAttribute.DefaultValue`][] property. This warning will not be emitted if the +[`CommandLineArgumentAttribute.DefaultValue`][] property is not null, regardless of the initializer. Note that default values set by property initializers are only shown in the usage help if the -`GeneratedParserAttribute` is used. When reflection is used, only -`CommandLineArgumentAttribute.DefaultValue` is supported. +[`GeneratedParserAttribute`][] is used. When reflection is used, only +[`CommandLineArgumentAttribute.DefaultValue`][] is supported. + +[`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AliasAttribute.htm +[`AllowDuplicateDictionaryKeysAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute.htm +[`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm +[`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm +[`CommandAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandAttribute_IsHidden.htm +[`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm +[`CommandLineArgument`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgument.htm +[`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[`CommandLineArgumentAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm +[`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm +[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm +[`CommandLineArgumentAttribute.IsRequired`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired.htm +[`CommandLineArgumentAttribute.IsShort`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort.htm +[`CommandLineArgumentAttribute.Position`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_Position.htm +[`CommandLineArgumentAttribute.ShortName`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm +[`CommandLineArgumentAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgumentAttribute.htm +[`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm +[`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser_1.htm +[`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm +[`DescriptionAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute +[`GeneratedCommandManagerAttribute.AssemblyNames`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute_AssemblyNames.htm +[`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm +[`GeneratedConverterNamespaceAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_GeneratedConverterNamespaceAttribute.htm +[`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`ICommand`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommand.htm +[`ICommandWithCustomParsing`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ICommandWithCustomParsing.htm +[`IsShort`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort.htm +[`KeyConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyConverterAttribute.htm +[`KeyValuePairConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyValuePairConverter_2.htm +[`KeyValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_KeyValueSeparatorAttribute.htm +[`MultiValueSeparatorAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_MultiValueSeparatorAttribute.htm +[`ParentCommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_ParentCommandAttribute.htm +[`ParseOptions.ArgumentNameComparison`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison.htm +[`ParseOptions.ArgumentNamePrefixes`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ArgumentNamePrefixes.htm +[`ParseOptions.ArgumentNameTransform`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_ArgumentNameTransform.htm +[`ParseOptions.Mode`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptions_Mode.htm +[`ParseOptionsAttribute.ArgumentNamePrefixes`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_ParseOptionsAttribute_ArgumentNamePrefixes.htm +[`ParsingMode.LongShort`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParsingMode.htm +[`ShortAliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ShortAliasAttribute.htm +[`Type`]: https://learn.microsoft.com/dotnet/api/system.type +[`TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute +[`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm +[IsHidden_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm +[IsRequired_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired.htm +[ShortName_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName.htm diff --git a/docs/refs.json b/docs/refs.json index 2075fbee..b869158a 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -42,7 +42,9 @@ "Category": "P_Ookii_CommandLine_CommandLineArgumentException_Category", "ClassValidationAttribute": "T_Ookii_CommandLine_Validation_ClassValidationAttribute", "CommandAttribute": "T_Ookii_CommandLine_Commands_CommandAttribute", + "CommandAttribute.IsHidden": "P_Ookii_CommandLine_Commands_CommandAttribute_IsHidden", "CommandInfo": "T_Ookii_CommandLine_Commands_CommandInfo", + "CommandLineArgument": "T_Ookii_CommandLine_CommandLineArgument", "CommandLineArgument.AllowNull": "P_Ookii_CommandLine_CommandLineArgument_AllowNull", "CommandLineArgument.ElementType": "P_Ookii_CommandLine_CommandLineArgument_ElementType", "CommandLineArgumentAttribute": "T_Ookii_CommandLine_CommandLineArgumentAttribute", @@ -77,6 +79,7 @@ "CommandManager.GetCommand()": "M_Ookii_CommandLine_Commands_CommandManager_GetCommand", "CommandManager.ParseResult": "P_Ookii_CommandLine_Commands_CommandManager_ParseResult", "CommandManager.RunCommandAsync()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync", + "CommandManagerAttribute": "!UNKNOWN!", "CommandNameComparison": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameComparison", "CommandNameTransform": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameTransform", "CommandOptions": "T_Ookii_CommandLine_Commands_CommandOptions", @@ -172,6 +175,12 @@ "IParsable": "#system.iparsable-1", "IParser": "T_Ookii_CommandLine_IParser_1", "IParserProvider": "T_Ookii_CommandLine_IParserProvider_1", + "IsHidden": [ + "P_Ookii_CommandLine_CommandLineArgument_IsHidden", + "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden", + "P_Ookii_CommandLine_Commands_CommandAttribute_IsHidden", + "P_Ookii_CommandLine_Commands_CommandInfo_IsHidden" + ], "ISpanParsable": "#system.ispanparsable-1", "IsPositional": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional", "IsPosix": [ @@ -179,10 +188,16 @@ "P_Ookii_CommandLine_ParseOptions_IsPosix", "P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix" ], + "IsRequired": [ + "P_Ookii_CommandLine_CommandLineArgument_IsRequired", + "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsRequired" + ], + "IsShort": "P_Ookii_CommandLine_CommandLineArgumentAttribute_IsShort", "IsValid()": "M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsValid", "KeyConverterAttribute": "T_Ookii_CommandLine_Conversion_KeyConverterAttribute", "KeyValuePair": "#system.collections.generic.keyvaluepair-2", "KeyValuePairConverter": "T_Ookii_CommandLine_Conversion_KeyValuePairConverter_2", + "KeyValueSeparatorAttribute": "T_Ookii_CommandLine_Conversion_KeyValueSeparatorAttribute", "LineWrappingTextWriter": "T_Ookii_CommandLine_LineWrappingTextWriter", "LineWrappingTextWriter.ForConsoleError()": "M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleError", "LineWrappingTextWriter.ForConsoleOut()": "M_Ookii_CommandLine_LineWrappingTextWriter_ForConsoleOut", @@ -241,18 +256,21 @@ "ParseOptions": "T_Ookii_CommandLine_ParseOptions", "ParseOptions.AllowWhiteSpaceValueSeparator": "P_Ookii_CommandLine_ParseOptions_AllowWhiteSpaceValueSeparator", "ParseOptions.ArgumentNameComparison": "P_Ookii_CommandLine_ParseOptions_ArgumentNameComparison", + "ParseOptions.ArgumentNamePrefixes": "P_Ookii_CommandLine_ParseOptions_ArgumentNamePrefixes", "ParseOptions.ArgumentNameTransform": "P_Ookii_CommandLine_ParseOptions_ArgumentNameTransform", "ParseOptions.AutoPrefixAliases": "P_Ookii_CommandLine_ParseOptions_AutoPrefixAliases", "ParseOptions.AutoVersionArgument": "P_Ookii_CommandLine_ParseOptions_AutoVersionArgument", "ParseOptions.Culture": "P_Ookii_CommandLine_ParseOptions_Culture", "ParseOptions.DefaultValueDescriptions": "P_Ookii_CommandLine_ParseOptions_DefaultValueDescriptions", "ParseOptions.IsPosix": "P_Ookii_CommandLine_ParseOptions_IsPosix", + "ParseOptions.Mode": "P_Ookii_CommandLine_ParseOptions_Mode", "ParseOptions.NameValueSeparators": "P_Ookii_CommandLine_ParseOptions_NameValueSeparators", "ParseOptions.ShowUsageOnError": "P_Ookii_CommandLine_ParseOptions_ShowUsageOnError", "ParseOptions.StringProvider": "P_Ookii_CommandLine_ParseOptions_StringProvider", "ParseOptions.UsageWriter": "P_Ookii_CommandLine_ParseOptions_UsageWriter", "ParseOptionsAttribute": "T_Ookii_CommandLine_ParseOptionsAttribute", "ParseOptionsAttribute.AllowWhiteSpaceValueSeparator": "P_Ookii_CommandLine_ParseOptionsAttribute_AllowWhiteSpaceValueSeparator", + "ParseOptionsAttribute.ArgumentNamePrefixes": "P_Ookii_CommandLine_ParseOptionsAttribute_ArgumentNamePrefixes", "ParseOptionsAttribute.AutoPrefixAliases": "P_Ookii_CommandLine_ParseOptionsAttribute_AutoPrefixAliases", "ParseOptionsAttribute.CaseSensitive": "P_Ookii_CommandLine_ParseOptionsAttribute_CaseSensitive", "ParseOptionsAttribute.IsPosix": "P_Ookii_CommandLine_ParseOptionsAttribute_IsPosix", @@ -302,6 +320,10 @@ "RunCommandAsync()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync", "SetConsoleMode": "https://learn.microsoft.com/windows/console/setconsolemode", "ShortAliasAttribute": "T_Ookii_CommandLine_ShortAliasAttribute", + "ShortName": [ + "P_Ookii_CommandLine_CommandLineArgument_ShortName", + "P_Ookii_CommandLine_CommandLineArgumentAttribute_ShortName" + ], "SomeName": null, "SortedDictionary": "#system.collections.generic.sorteddictionary-2", "StreamReader": "#system.io.streamreader", From 2aa281d6ce120e6e55dd7a8ae48cda743e2c7315 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 12:41:47 -0700 Subject: [PATCH 167/234] Code snippets updates. --- docs/CodeSnippets.md | 8 +-- src/Snippets/CSharp/clargclass.snippet | 16 ++--- src/Snippets/CSharp/clargpos.snippet | 16 +---- src/Snippets/CSharp/clargreq.snippet | 61 +++++++++++++++++++ src/Snippets/CSharp/clcmd.snippet | 9 +-- src/Snippets/CSharp/clcmdasync.snippet | 9 +-- .../Ookii.CommandLine.Snippets.csproj | 5 ++ src/Snippets/source.extension.vsixmanifest | 6 +- 8 files changed, 90 insertions(+), 40 deletions(-) create mode 100644 src/Snippets/CSharp/clargreq.snippet 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/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..4f17a6ce --- /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/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 From a68515b7b3564db6dc1238c5eb872b914b402639 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 12:43:36 -0700 Subject: [PATCH 168/234] Remove outdated readme. --- src/Snippets/README.md | 3 +++ src/Snippets/Readme.txt | 23 ----------------------- 2 files changed, 3 insertions(+), 23 deletions(-) create mode 100644 src/Snippets/README.md delete mode 100644 src/Snippets/Readme.txt 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. From a22b3ab0f620e191246d268c142b6576623337a1 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 13:01:35 -0700 Subject: [PATCH 169/234] Fix snippet mistake. --- src/Snippets/CSharp/clargreq.snippet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Snippets/CSharp/clargreq.snippet b/src/Snippets/CSharp/clargreq.snippet index 4f17a6ce..d75a4b29 100644 --- a/src/Snippets/CSharp/clargreq.snippet +++ b/src/Snippets/CSharp/clargreq.snippet @@ -27,7 +27,7 @@ Type Property and argument type - string? + string From d41b29b5ccea78d9f5df139498ab2dad6ceb3cb5 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 13:15:53 -0700 Subject: [PATCH 170/234] Ran code cleanup. --- src/.editorconfig | 4 +- .../CommandGenerator.cs | 3 +- .../ConverterGenerator.cs | 4 +- src/Ookii.CommandLine.Generator/Extensions.cs | 3 - .../ParserGenerator.cs | 23 +- .../ParserIncrementalGenerator.cs | 1 - src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 2 +- .../ArgumentValidatorTest.cs | 271 ++-- .../CommandLineParserTest.Usage.cs | 59 +- .../CommandLineParserTest.cs | 6 +- .../LineWrappingTextWriterTest.Constants.cs | 35 +- .../LineWrappingTextWriterTest.cs | 910 ++++++----- .../NameTransformTest.cs | 95 +- .../ParseOptionsAttributeTest.cs | 2 - src/Ookii.CommandLine.Tests/SubCommandTest.cs | 6 +- src/Ookii.CommandLine/AliasAttribute.cs | 116 +- .../AllowDuplicateDictionaryKeysAttribute.cs | 48 +- .../ApplicationFriendlyNameAttribute.cs | 83 +- src/Ookii.CommandLine/ArgumentKind.cs | 45 +- .../ArgumentParsedEventArgs.cs | 3 +- src/Ookii.CommandLine/BreakLineMode.cs | 13 +- src/Ookii.CommandLine/CommandLineArgument.cs | 10 +- .../CommandLineArgumentAttribute.cs | 1 - .../CommandLineArgumentErrorCategory.cs | 130 +- .../CommandLineArgumentException.cs | 288 ++-- src/Ookii.CommandLine/CommandLineParser.cs | 5 +- .../CommandLineParserGeneric.cs | 192 ++- .../Commands/AsyncCommandBase.cs | 29 +- .../Commands/AutomaticVersionCommand.cs | 2 - .../Commands/AutomaticVersionCommandInfo.cs | 2 - .../Commands/CommandAttribute.cs | 138 +- src/Ookii.CommandLine/Commands/CommandInfo.cs | 668 ++++---- .../Commands/CommandManager.cs | 1 - .../Commands/CommandOptions.cs | 401 +++-- .../Commands/IAsyncCommand.cs | 55 +- src/Ookii.CommandLine/Commands/ICommand.cs | 53 +- .../Commands/ICommandWithCustomParsing.cs | 41 +- .../Commands/ParentCommand.cs | 3 - .../Conversion/KeyValuePairConverter.cs | 3 +- .../TypeConverterArgumentConverter.cs | 1 - .../DescriptionListFilterMode.cs | 45 +- .../DescriptionListSortMode.cs | 63 +- src/Ookii.CommandLine/DisposableWrapper.cs | 84 +- .../DuplicateArgumentEventArgs.cs | 123 +- src/Ookii.CommandLine/ErrorMode.cs | 35 +- .../LineWrappingTextWriter.cs | 1412 ++++++++--------- .../LocalizedStringProvider.Error.cs | 305 ++-- .../LocalizedStringProvider.Validators.cs | 525 +++--- .../LocalizedStringProvider.cs | 217 ++- .../MultiValueSeparatorAttribute.cs | 186 ++- src/Ookii.CommandLine/NameTransform.cs | 75 +- .../NameTransformExtensions.cs | 197 ++- src/Ookii.CommandLine/NativeMethods.cs | 151 +- src/Ookii.CommandLine/ParseResult.cs | 203 ++- src/Ookii.CommandLine/ParseStatus.cs | 45 +- src/Ookii.CommandLine/ParsingMode.cs | 43 +- src/Ookii.CommandLine/ShortAliasAttribute.cs | 94 +- src/Ookii.CommandLine/StringExtensions.cs | 73 +- src/Ookii.CommandLine/StringSegmentType.cs | 27 +- src/Ookii.CommandLine/StringSpan.Async.cs | 99 +- src/Ookii.CommandLine/StringSpanExtensions.cs | 169 +- src/Ookii.CommandLine/StringSpanTuple.cs | 29 +- .../Support/ArgumentProvider.cs | 3 +- .../Support/GeneratedArgument.cs | 6 +- .../Support/GeneratedArgumentProvider.cs | 3 - .../Support/GeneratedCommandInfo.cs | 2 +- .../Support/ReflectionArgument.cs | 12 +- .../Support/ReflectionArgumentProvider.cs | 2 - .../Support/ReflectionCommandInfo.cs | 2 - .../Terminal/StandardStream.cs | 33 +- src/Ookii.CommandLine/Terminal/TextFormat.cs | 363 +++-- .../Terminal/VirtualTerminal.cs | 285 ++-- .../Terminal/VirtualTerminalSupport.cs | 101 +- src/Ookii.CommandLine/TypeHelper.cs | 254 ++- src/Ookii.CommandLine/UsageHelpRequest.cs | 39 +- src/Ookii.CommandLine/UsageWriter.cs | 2 +- .../Validation/ArgumentValidationAttribute.cs | 383 +++-- .../ArgumentValidationWithHelpAttribute.cs | 101 +- .../Validation/ClassValidationAttribute.cs | 179 ++- .../DependencyValidationAttribute.cs | 227 ++- .../Validation/ProhibitsAttribute.cs | 113 +- .../Validation/RequiresAnyAttribute.cs | 351 ++-- .../Validation/RequiresAttribute.cs | 113 +- .../Validation/ValidateCountAttribute.cs | 167 +- .../Validation/ValidateEnumValueAttribute.cs | 141 +- .../Validation/ValidateNotEmptyAttribute.cs | 115 +- .../Validation/ValidateNotNullAttribute.cs | 101 +- .../ValidateNotWhiteSpaceAttribute.cs | 117 +- .../Validation/ValidatePatternAttribute.cs | 209 ++- .../Validation/ValidateRangeAttribute.cs | 176 +- .../ValidateStringLengthAttribute.cs | 153 +- .../Validation/ValidationMode.cs | 47 +- src/Ookii.CommandLine/WrappingMode.cs | 41 +- 93 files changed, 5831 insertions(+), 5965 deletions(-) diff --git a/src/.editorconfig b/src/.editorconfig index 64a8405b..74546908 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -112,7 +112,7 @@ csharp_preferred_modifier_order = public,private,protected,internal,static,exter # Code-block preferences csharp_prefer_braces = true:warning csharp_prefer_simple_using_statement = true:suggestion -csharp_style_namespace_declarations = block_scoped:silent +csharp_style_namespace_declarations = file_scoped:warning csharp_style_prefer_method_group_conversion = true:silent # Expression-level preferences @@ -376,6 +376,8 @@ dotnet_diagnostic.IDE0076.severity = warning dotnet_diagnostic.IDE0077.severity = warning dotnet_diagnostic.IDE0073.severity = silent dotnet_diagnostic.IDE0060.severity = warning +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_utf8_string_literals = true:suggestion [*.{cs,vb}] dotnet_style_operator_placement_when_wrapping = beginning_of_line diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index 924244c1..f8905931 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -1,6 +1,5 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; -using System.Diagnostics; using System.Text; namespace Ookii.CommandLine.Generator; @@ -256,7 +255,7 @@ private bool GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType } IAssemblySymbol? foundAssembly = null; - foreach (var reference in _typeHelper.Compilation.References) + foreach (var reference in _typeHelper.Compilation.References) { if (_typeHelper.Compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) { diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index f019d4c2..84ad32fd 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -1,7 +1,5 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using System.Text; -using System.Xml.Linq; namespace Ookii.CommandLine.Generator; @@ -252,7 +250,7 @@ private static string GetGeneratedNamespace(TypeHelper typeHelper, SourceProduct var elements = ns.Split('.'); foreach (var element in elements) { - if (!SyntaxFacts.IsValidIdentifier(element)) + if (!SyntaxFacts.IsValidIdentifier(element)) { context.ReportDiagnostic(Diagnostics.InvalidGeneratedConverterNamespace(ns, attribute)); return DefaultGeneratedNamespace; diff --git a/src/Ookii.CommandLine.Generator/Extensions.cs b/src/Ookii.CommandLine.Generator/Extensions.cs index dffbd306..0bb248b7 100644 --- a/src/Ookii.CommandLine.Generator/Extensions.cs +++ b/src/Ookii.CommandLine.Generator/Extensions.cs @@ -1,9 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using System; -using System.Diagnostics; using System.Text; -using static System.Net.Mime.MediaTypeNames; namespace Ookii.CommandLine.Generator; diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 5192f89c..fe67dbc5 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -1,14 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Operations; -using System; -using System.Data; using System.Diagnostics; -using System.Globalization; -using System.Reflection; -using System.Text; -using System.Xml.Linq; namespace Ookii.CommandLine.Generator; @@ -60,7 +53,7 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen _builder = new(argumentsClass.ContainingNamespace); _converterGenerator = converterGenerator; _commandGenerator = commandGenerator; - _languageVersion = languageVersion; + _languageVersion = languageVersion; } public static string? Generate(SourceProductionContext context, INamedTypeSymbol argumentsClass, TypeHelper typeHelper, @@ -395,7 +388,7 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> return false; } - var separator = attributes.KeyValueSeparator == null + var separator = attributes.KeyValueSeparator == null ? "null" : $"keyValueSeparatorAttribute{member.Name}.Separator"; @@ -532,7 +525,7 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> else { arguments = $"({originalArgumentType.ToQualifiedName()})value{notNullAnnotation}"; - } + } } else if (info.HasParserParameter) { @@ -851,7 +844,7 @@ private bool VerifyPositionalArgumentRules() string? multiValueArgument = null; string? optionalArgument = null; var result = true; - foreach (var argument in _positionalArguments) + foreach (var argument in _positionalArguments) { if (multiValueArgument != null) { @@ -882,8 +875,8 @@ private bool VerifyPositionalArgumentRules() private void CheckIgnoredDictionaryAttribute(ISymbol member, bool isDictionary, AttributeData? converter, AttributeData? attribute) { if (attribute == null) - { - return; + { + return; } if (!isDictionary) @@ -905,8 +898,8 @@ private void CheckIgnoredDictionaryAttribute(ISymbol member, bool isDictionary, } var expression = syntax.Initializer.Value; - if (expression is PostfixUnaryExpressionSyntax postfixUnaryExpression) - { + if (expression is PostfixUnaryExpressionSyntax postfixUnaryExpression) + { if (postfixUnaryExpression.Kind() == SyntaxKind.SuppressNullableWarningExpression) { expression = postfixUnaryExpression.Operand; diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index dc8a2351..8897dc31 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -3,7 +3,6 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using System.Collections.Immutable; -using System.Diagnostics; using System.Text; namespace Ookii.CommandLine.Generator; diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 67b8e4b1..7c6e174b 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -155,7 +155,7 @@ partial class MultiValueSeparatorArguments public string[] Separator { get; set; } } -[GeneratedParser] +[GeneratedParser] partial class SimpleArguments { [CommandLineArgument] diff --git a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs index 582583b6..b169e15d 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs @@ -3,143 +3,142 @@ 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 + 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() { - 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)); - } + 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)); } } diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.Usage.cs index d9ce0a88..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 { - 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 b8fd7183..f2433a82 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1,10 +1,8 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Ookii.CommandLine.Support; using Ookii.CommandLine.Tests.Commands; using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; @@ -1533,7 +1531,7 @@ public static IEnumerable ProviderKinds }; public static void AssertSpanEqual(ReadOnlySpan expected, ReadOnlySpan actual) - where T: IEquatable + where T : IEquatable { if (!expected.SequenceEqual(actual)) { 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..be3aab67 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/ParseOptionsAttributeTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs index 878d01ab..021ae087 100644 --- a/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs +++ b/src/Ookii.CommandLine.Tests/ParseOptionsAttributeTest.cs @@ -1,6 +1,4 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using Ookii.CommandLine.Terminal; -using System; namespace Ookii.CommandLine.Tests; diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index c759dd09..5a074013 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -1,11 +1,9 @@ -// 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.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -470,7 +468,7 @@ public void TestParentCommandUsage(ProviderKind kind) public void TestAutoPrefixAliases(ProviderKind kind) { var manager = CreateManager(kind); - + // Ambiguous between test and TestParentCommand. Assert.IsNull(manager.GetCommand("tes")); diff --git a/src/Ookii.CommandLine/AliasAttribute.cs b/src/Ookii.CommandLine/AliasAttribute.cs index 69c2d10e..59d1129f 100644 --- a/src/Ookii.CommandLine/AliasAttribute.cs +++ b/src/Ookii.CommandLine/AliasAttribute.cs @@ -1,67 +1,65 @@ -// 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 a 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..cd5738d8 100644 --- a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs +++ b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs @@ -1,29 +1,27 @@ -// 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 +/// , 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 { - /// - /// 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..ee38c521 100644 --- a/src/Ookii.CommandLine/ApplicationFriendlyNameAttribute.cs +++ b/src/Ookii.CommandLine/ApplicationFriendlyNameAttribute.cs @@ -1,50 +1,49 @@ using System; -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. 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 { + 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/ArgumentParsedEventArgs.cs b/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs index b7a88309..5440f44c 100644 --- a/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs +++ b/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs @@ -1,5 +1,4 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; +using System; namespace Ookii.CommandLine; 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/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index c7bbcb2e..f0d419c3 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1,11 +1,9 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Conversion; using Ookii.CommandLine.Support; using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Globalization; @@ -182,7 +180,7 @@ private class HelpArgument : CommandLineArgument { public HelpArgument(CommandLineParser parser, string argumentName, char shortName, char shortAlias) : base(CreateInfo(parser, argumentName, shortName, shortAlias)) - { + { } protected override bool CanSetProperty => false; @@ -1186,7 +1184,7 @@ public override string ToString() /// /// The value description. protected virtual string DetermineValueDescriptionForType(Type type) => GetFriendlyTypeName(type); - + internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, Type argumentType, bool allowsNull, @@ -1591,7 +1589,7 @@ internal static string DetermineArgumentName(string? explicitName, string member private string? GetDefaultValueDescription(Type? type) { - if (Parser.Options.DefaultValueDescriptions == null || + if (Parser.Options.DefaultValueDescriptions == null || !Parser.Options.DefaultValueDescriptions.TryGetValue(type ?? ElementType, out string? value)) { return null; diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index 43c53cc6..bb8d6f04 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -1,6 +1,5 @@ using Ookii.CommandLine.Commands; using System; -using System.ComponentModel; namespace Ookii.CommandLine; diff --git a/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs b/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs index 704a0a5d..beed6a69 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs @@ -1,71 +1,69 @@ -// 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 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, } diff --git a/src/Ookii.CommandLine/CommandLineArgumentException.cs b/src/Ookii.CommandLine/CommandLineArgumentException.cs index ed31ade2..a5097560 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentException.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentException.cs @@ -1,167 +1,165 @@ -// 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 +/// 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; + /// - /// 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. + /// + /// 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. + /// 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 parameter 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/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 419bed49..e415b4f9 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1,13 +1,10 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Ookii.CommandLine.Commands; +using Ookii.CommandLine.Commands; using Ookii.CommandLine.Support; using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Collections.ObjectModel; using System.ComponentModel; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index 7c7cd298..3e04b5e8 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -1,119 +1,117 @@ -using Ookii.CommandLine.Commands; -using Ookii.CommandLine.Support; +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 +/// methods. +/// +/// The type that defines the arguments. +/// +/// +/// 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. +/// +/// +/// 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. +/// +/// +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. - /// - /// - /// 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. - /// + /// /// - public class CommandLineParser : CommandLineParser - where T : class - { - /// - /// 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. - /// - /// - /// - /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = UnreferencedCodeHelpUrl)] + [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) - { - } + public CommandLineParser(ParseOptions? options = null) + : base(typeof(T), options) + { + } - /// - /// 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) + /// + /// 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)) { - if (provider.ArgumentsType != typeof(T)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.IncorrectProviderTypeFormat, typeof(T)), nameof(provider)); - } + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.IncorrectProviderTypeFormat, typeof(T)), nameof(provider)); } + } - /// - public new T? Parse() - { - return (T?)base.Parse(); - } + /// + public new T? Parse() + { + return (T?)base.Parse(); + } - /// - public new T? Parse(string[] args, int index = 0) - { - return (T?)base.Parse(args, index); - } + /// + public new T? Parse(string[] args, int index = 0) + { + return (T?)base.Parse(args, index); + } - /// - public new T? Parse(ReadOnlyMemory args) - { - return (T?)base.Parse(args); - } + /// + public new T? Parse(ReadOnlyMemory args) + { + return (T?)base.Parse(args); + } - /// - public new T? ParseWithErrorHandling() - { - return (T?)base.ParseWithErrorHandling(); - } + /// + public new T? ParseWithErrorHandling() + { + return (T?)base.ParseWithErrorHandling(); + } - /// - public new T? ParseWithErrorHandling(string[] args, int index = 0) - { - return (T?)base.ParseWithErrorHandling(args, index); - } + /// + public new T? ParseWithErrorHandling(string[] args, int index = 0) + { + return (T?)base.ParseWithErrorHandling(args, index); + } - /// - public new T? ParseWithErrorHandling(ReadOnlyMemory 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..1ef17765 100644 --- a/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs +++ b/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs @@ -1,23 +1,22 @@ using System.Threading.Tasks; -namespace Ookii.CommandLine.Commands +namespace Ookii.CommandLine.Commands; + +/// +/// Base class for asynchronous tasks that want the method to +/// invoke 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 60ad1426..b258e1d0 100644 --- a/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs +++ b/src/Ookii.CommandLine/Commands/AutomaticVersionCommand.cs @@ -1,8 +1,6 @@ using Ookii.CommandLine.Support; -using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; -using System.ComponentModel; using System.Reflection; namespace Ookii.CommandLine.Commands; diff --git a/src/Ookii.CommandLine/Commands/AutomaticVersionCommandInfo.cs b/src/Ookii.CommandLine/Commands/AutomaticVersionCommandInfo.cs index 4fe9de07..040aecca 100644 --- a/src/Ookii.CommandLine/Commands/AutomaticVersionCommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/AutomaticVersionCommandInfo.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Ookii.CommandLine.Commands; diff --git a/src/Ookii.CommandLine/Commands/CommandAttribute.cs b/src/Ookii.CommandLine/Commands/CommandAttribute.cs index 424cebc7..1e023601 100644 --- a/src/Ookii.CommandLine/Commands/CommandAttribute.cs +++ b/src/Ookii.CommandLine/Commands/CommandAttribute.cs @@ -1,86 +1,84 @@ -// 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 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 +/// property. +/// +/// +/// A command can be given more than one name 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 /// 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 locate 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 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 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 aed2e44c..bc73c6db 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -2,389 +2,385 @@ 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.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 abstract class 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 . + /// + /// + /// is not a command type. + /// + 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; + _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 class. - /// - /// 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 . - /// - /// - /// is not a command type. - /// - protected CommandInfo(Type commandType, CommandAttribute attribute, CommandManager manager, Type? parentCommandType) - { - _manager = manager ?? throw new ArgumentNullException(nameof(manager)); - _name = GetName(attribute, commandType, manager.Options); - _commandType = commandType; - _attribute = attribute; - ParentCommandType = parentCommandType; - } + internal CommandInfo(Type commandType, string name, CommandManager manager) + { + _manager = manager; + _name = name; + _commandType = commandType; + _attribute = new(); + } - internal CommandInfo(Type commandType, string name, CommandManager manager) - { - _manager = manager; - _name = name; - _commandType = commandType; - _attribute = new(); - } + /// + /// Gets the that this instance belongs to. + /// + /// + /// An instance of the class. + /// + public CommandManager Manager => _manager; - /// - /// Gets the that this instance belongs to. - /// - /// - /// An instance of the class. - /// - public CommandManager Manager => _manager; + /// + /// 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 abstract string? Description { get; } + /// + /// 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 abstract bool UseCustomArgumentParsing { get; } + /// + /// 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 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 abstract IEnumerable Aliases { get; } + /// + /// 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; } - /// - /// 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. - /// - /// 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) + /// + /// 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) + { + if (args == null) { - var (command, _) = CreateInstanceWithResult(args, index); - return command; + throw new ArgumentNullException(nameof(index)); } - /// - /// 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) + if (index < 0 || index > args.Length) { - if (args == null) - { - throw new ArgumentNullException(nameof(index)); - } + throw new ArgumentOutOfRangeException(nameof(index)); + } - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } + return CreateInstanceWithResult(args.AsMemory(index)); + } - return CreateInstanceWithResult(args.AsMemory(index)); + /// + /// Creates an instance of the command type. + /// + /// 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. + /// + /// + /// + /// The property of the returned + /// will be if the command used custom parsing. + /// + /// + public (ICommand?, ParseResult) CreateInstanceWithResult(ReadOnlyMemory args) + { + if (UseCustomArgumentParsing) + { + var command = CreateInstanceWithCustomParsing(); + command.Parse(args, _manager); + return (command, default); } - - /// - /// Creates an instance of the command type. - /// - /// 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. - /// - /// - /// - /// The property of the returned - /// will be if the command used custom parsing. - /// - /// - public (ICommand?, ParseResult) CreateInstanceWithResult(ReadOnlyMemory args) + else { - if (UseCustomArgumentParsing) - { - var command = CreateInstanceWithCustomParsing(); - command.Parse(args, _manager); - return (command, default); - } - else - { - var parser = CreateParser(); - var command = (ICommand?)parser.ParseWithErrorHandling(args); - return (command, parser.ParseResult); - } + var parser = CreateParser(); + var command = (ICommand?)parser.ParseWithErrorHandling(args); + return (command, parser.ParseResult); } + } - /// - /// 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 abstract CommandLineParser CreateParser(); + /// + /// 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 abstract CommandLineParser CreateParser(); - /// - /// Creates an instance of a command that uses the - /// interface. - /// - /// An instance of the command type. - /// - /// The command does not use the interface. - /// - public abstract ICommandWithCustomParsing CreateInstanceWithCustomParsing(); + /// + /// Creates an instance of a command that uses the + /// interface. + /// + /// An instance of the command type. + /// + /// The command does not use the interface. + /// + public abstract ICommandWithCustomParsing CreateInstanceWithCustomParsing(); - /// - /// 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. - /// - /// - /// is . - /// - public bool MatchesName(string name) + /// + /// 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. + /// + /// + /// is . + /// + public bool MatchesName(string name) + { + if (name == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (string.Equals(name, _name, Manager.Options.CommandNameComparison)) - { - return true; - } - - return Aliases.Any(alias => string.Equals(name, alias, Manager.Options.CommandNameComparison)); + throw new ArgumentNullException(nameof(name)); } - /// - /// Checks whether the command's name or one of its aliases start with the specified prefix. - /// - /// The prefix to check for. - /// - /// if the is a prefix of the - /// property or any of the items in the property. - /// - /// - /// is . - /// - public bool MatchesPrefix(string prefix) + if (string.Equals(name, _name, Manager.Options.CommandNameComparison)) { - if (prefix == null) - { - throw new ArgumentNullException(nameof(prefix)); - } + return true; + } + + return Aliases.Any(alias => string.Equals(name, alias, Manager.Options.CommandNameComparison)); + } - if (Name.StartsWith(prefix, Manager.Options.CommandNameComparison)) - { - return true; - } + /// + /// Checks whether the command's name or one of its aliases start with the specified prefix. + /// + /// The prefix to check for. + /// + /// if the is a prefix of the + /// property or any of the items in the property. + /// + /// + /// is . + /// + public bool MatchesPrefix(string prefix) + { + if (prefix == null) + { + throw new ArgumentNullException(nameof(prefix)); + } - return Aliases.Any(alias => alias.StartsWith(prefix, Manager.Options.CommandNameComparison)); + if (Name.StartsWith(prefix, Manager.Options.CommandNameComparison)) + { + return true; } - /// - /// 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 . - /// - /// - /// A class with information about the command, or - /// if was not a command. - /// + return Aliases.Any(alias => alias.StartsWith(prefix, Manager.Options.CommandNameComparison)); + } + + /// + /// 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 . + /// + /// + /// A class with information about the command, or + /// if was not a command. + /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] + [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); + public static CommandInfo? TryCreate(Type commandType, CommandManager manager) + => ReflectionCommandInfo.TryCreate(commandType, manager); - /// - /// 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 not a command. - /// - /// - /// A class with information about the command. - /// + /// + /// 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 not a command. + /// + /// + /// 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)] + [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); + public static CommandInfo Create(Type commandType, CommandManager manager) + => new ReflectionCommandInfo(commandType, null, manager); - /// - /// 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 . - /// + /// + /// 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 . + /// #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] + [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] #endif - public static bool IsCommand(Type commandType) => ReflectionCommandInfo.GetCommandAttribute(commandType) != null; + public static bool IsCommand(Type commandType) => ReflectionCommandInfo.GetCommandAttribute(commandType) != null; - internal static CommandInfo GetAutomaticVersionCommand(CommandManager manager) - => new AutomaticVersionCommandInfo(manager); + internal static CommandInfo GetAutomaticVersionCommand(CommandManager manager) + => new AutomaticVersionCommandInfo(manager); - private static string GetName(CommandAttribute attribute, Type commandType, CommandOptions? options) - { - return attribute.CommandName ?? - options?.CommandNameTransform.Apply(commandType.Name, options.StripCommandNameSuffix) ?? - commandType.Name; - } + 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 fbf23a15..0375c511 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; diff --git a/src/Ookii.CommandLine/Commands/CommandOptions.cs b/src/Ookii.CommandLine/Commands/CommandOptions.cs index aa7d1ac1..5689eeeb 100644 --- a/src/Ookii.CommandLine/Commands/CommandOptions.cs +++ b/src/Ookii.CommandLine/Commands/CommandOptions.cs @@ -1,220 +1,217 @@ -// 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. "argument-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 and + /// 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 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 and command names are case - /// sensitive, and argument names, command 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 , - /// 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 and - /// 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 - { - get => base.IsPosix && CommandNameComparison.IsCaseSensitive() && CommandNameTransform == NameTransform.DashCase; - set + get => base.IsPosix && CommandNameComparison.IsCaseSensitive() && CommandNameTransform == NameTransform.DashCase; + set + { + base.IsPosix = value; + if (value) + { + CommandNameComparison = StringComparison.InvariantCulture; + CommandNameTransform = NameTransform.DashCase; + } + else { - base.IsPosix = value; - if (value) - { - CommandNameComparison = StringComparison.InvariantCulture; - CommandNameTransform = NameTransform.DashCase; - } - else - { - CommandNameComparison = StringComparison.OrdinalIgnoreCase; - CommandNameTransform = NameTransform.None; - } + CommandNameComparison = StringComparison.OrdinalIgnoreCase; + CommandNameTransform = NameTransform.None; } } + } - /// - /// Gets or set 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 set 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 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 suffix is case sensitive. - /// - /// - public string? StripCommandNameSuffix { get; set; } = "Command"; + /// + /// 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 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 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 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 command manager. - /// - /// - public Type? ParentCommand { get; set; } + /// + /// 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 command manager. + /// + /// + public Type? ParentCommand { 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. - /// - /// - 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 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 argument. 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; + /// + /// Gets or sets a value that indicates whether unique prefixes of a command name 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 argument. 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/IAsyncCommand.cs b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs index efd93590..f9cd133b 100644 --- a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs +++ b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs @@ -1,38 +1,37 @@ 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. +/// +/// +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 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. /// /// - 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..a8ddb179 100644 --- a/src/Ookii.CommandLine/Commands/ICommand.cs +++ b/src/Ookii.CommandLine/Commands/ICommand.cs @@ -1,35 +1,34 @@ -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 , 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. +/// +/// +/// +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 applications 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 9d8cd567..8000b154 100644 --- a/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs +++ b/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs @@ -1,27 +1,26 @@ using System; -namespace Ookii.CommandLine.Commands +namespace Ookii.CommandLine.Commands; + +/// +/// Represents a subcommand that does its own argument parsing. +/// +/// +/// +/// 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 { /// - /// 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 that was used to create this command. - void Parse(ReadOnlyMemory args, CommandManager manager); - } + /// The arguments. + /// 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 index 2fb79d87..c5de8349 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommand.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommand.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace Ookii.CommandLine.Commands; diff --git a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs index 594e9357..0b3b0b59 100644 --- a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs +++ b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs @@ -1,5 +1,4 @@ -// Copyright (c) Sven Groot (Ookii.org) -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; diff --git a/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs b/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs index 9c4113ec..fbdb9bb4 100644 --- a/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs +++ b/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Ookii.CommandLine.Conversion; diff --git a/src/Ookii.CommandLine/DescriptionListFilterMode.cs b/src/Ookii.CommandLine/DescriptionListFilterMode.cs index 827b8264..8bdd1a85 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 printing usage. +/// +/// +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..9972d670 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. +/// +/// +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 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/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 3a3929a0..4cd6c4ea 100644 --- a/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs +++ b/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs @@ -1,73 +1,72 @@ 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 new value of the argument. + /// + /// is + /// + public DuplicateArgumentEventArgs(CommandLineArgument argument, string? newValue) { - private readonly CommandLineArgument _argument; - private readonly ReadOnlyMemory _memoryValue; - private readonly string? _stringValue; - private bool _hasValue; - - /// - /// 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)); - _stringValue = newValue; - _hasValue = newValue != null; - } + _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. - /// if the argument has a value; otherwise, . - /// The 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; - } + /// + /// Initializes a new instance of the class. + /// + /// The argument that was specified more than once. + /// if the argument has a value; otherwise, . + /// The 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 => _hasValue ? (_stringValue ?? _memoryValue.ToString()) : null; + /// + /// 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 => _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..197621b6 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/LineWrappingTextWriter.cs b/src/Ookii.CommandLine/LineWrappingTextWriter.cs index 512dcf08..303fe627 100644 --- a/src/Ookii.CommandLine/LineWrappingTextWriter.cs +++ b/src/Ookii.CommandLine/LineWrappingTextWriter.cs @@ -1,870 +1,866 @@ -// 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 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 { - /// - /// 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; } + } + + /// + public override Encoding Encoding + { + get { return _baseWriter.Encoding; } + } + + /// #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 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 { - 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, first call , then use the method of 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); - } - - _asyncWriteTask = WriteCoreAsync(buffer, cancellationToken); - 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 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(); - } + /// + 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 WriteNoMaximum(ReadOnlySpan buffer); + + private partial void WriteLineBreakDirect(); - private partial void WriteIndentDirectIfNeeded(); + private partial void WriteIndentDirectIfNeeded(); - private static partial void WriteIndent(TextWriter writer, int indent); + private static partial void WriteIndent(TextWriter writer, int indent); - private partial void WriteCore(ReadOnlySpan buffer); + private partial void WriteCore(ReadOnlySpan buffer); - private partial void FlushCore(bool insertNewLine); + private partial void FlushCore(bool insertNewLine); - private partial void ResetIndentCore(); + private partial void ResetIndentCore(); - private static partial void WriteBlankLine(TextWriter writer); + private static partial void WriteBlankLine(TextWriter writer); - private static int GetLineLengthForConsole() + 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 b995ecfa..acf8c150 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs @@ -3,161 +3,160 @@ 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 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) { - /// - /// 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..d67af141 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs @@ -4,307 +4,306 @@ 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 class. + /// + /// 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 class. + /// + /// 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 class. + /// + /// 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 class. + /// + /// 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 class. + /// + /// 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 class. + /// + /// 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 class. + /// + /// 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 class. + /// + /// 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 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 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 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 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. + /// + /// 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 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 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 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 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 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 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..6996ec66 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.cs @@ -3,130 +3,129 @@ 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 using . +/// +/// +/// 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 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. /// - /// - /// 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 , + /// 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/MultiValueSeparatorAttribute.cs b/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs index 248b6398..e26e2afd 100644 --- a/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs +++ b/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs @@ -1,109 +1,107 @@ -// 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 +/// 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 been 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..1b0d7eff 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 property, parameter, or method name if an argument doesn't +/// have an explicit 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..e01fb4d1 100644 --- a/src/Ookii.CommandLine/NameTransformExtensions.cs +++ b/src/Ookii.CommandLine/NameTransformExtensions.cs @@ -1,127 +1,126 @@ 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..17ad25d7 100644 --- a/src/Ookii.CommandLine/NativeMethods.cs +++ b/src/Ookii.CommandLine/NativeMethods.cs @@ -2,95 +2,94 @@ using System; using System.Runtime.InteropServices; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +static 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 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 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); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern IntPtr GetStdHandle(StandardHandle nStdHandle); + if (!GetConsoleMode(handle, out ConsoleModes mode)) + { + return null; + } - [Flags] - public enum ConsoleModes : uint + var oldMode = mode; + if (enable) { - 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 + mode |= ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } + else + { + mode &= ~ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING; } - private enum StandardHandle + if (!SetConsoleMode(handle, mode)) { - STD_OUTPUT_HANDLE = -11, - STD_INPUT_HANDLE = -10, - STD_ERROR_HANDLE = -12, + return null; } + + return 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); + } + + [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); + + [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, + + 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 + } + + private enum StandardHandle + { + STD_OUTPUT_HANDLE = -11, + STD_INPUT_HANDLE = -10, + STD_ERROR_HANDLE = -12, } } diff --git a/src/Ookii.CommandLine/ParseResult.cs b/src/Ookii.CommandLine/ParseResult.cs index af6fea08..0e599ba1 100644 --- a/src/Ookii.CommandLine/ParseResult.cs +++ b/src/Ookii.CommandLine/ParseResult.cs @@ -1,115 +1,114 @@ using System; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates the result of the last call to the +/// method. +/// +/// +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, - ReadOnlyMemory remainingArguments = default) - { - Status = status; - LastException = exception; - ArgumentName = argumentName; - RemainingArguments = remainingArguments; - } + /// + /// 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 - /// , 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 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. - /// - /// - /// The name of the argument that canceled parsing using . - /// - /// 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); + /// + /// Gets a instance that represents successful parsing. + /// + /// + /// The name of the argument that canceled parsing using . + /// + /// 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. - /// 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 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. - /// 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); - } + /// + /// 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..38b58c15 100644 --- a/src/Ookii.CommandLine/ParseStatus.cs +++ b/src/Ookii.CommandLine/ParseStatus.cs @@ -1,27 +1,26 @@ -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +/// +/// Indicates the status of the last call to the +/// method. +/// +/// +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 was successful. + /// + Success, + /// + /// An error occurred while parsing the arguments. + /// + Error, + /// + /// Parsing was canceled by one of the arguments. + /// + Canceled } diff --git a/src/Ookii.CommandLine/ParsingMode.cs b/src/Ookii.CommandLine/ParsingMode.cs index 205cd9c5..29c6e468 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/ShortAliasAttribute.cs b/src/Ookii.CommandLine/ShortAliasAttribute.cs index 900a47a6..779aa058 100644 --- a/src/Ookii.CommandLine/ShortAliasAttribute.cs +++ b/src/Ookii.CommandLine/ShortAliasAttribute.cs @@ -1,55 +1,53 @@ -// 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 +/// 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 { + 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/StringExtensions.cs b/src/Ookii.CommandLine/StringExtensions.cs index e02c4b03..117dd160 100644 --- a/src/Ookii.CommandLine/StringExtensions.cs +++ b/src/Ookii.CommandLine/StringExtensions.cs @@ -1,51 +1,50 @@ using System; -namespace Ookii.CommandLine +namespace Ookii.CommandLine; + +internal static class StringExtensions { - internal static class StringExtensions + public static (ReadOnlyMemory, ReadOnlyMemory?) SplitOnce(this ReadOnlyMemory value, char separator) { - public static (ReadOnlyMemory, ReadOnlyMemory?) SplitOnce(this ReadOnlyMemory value, char separator) - { - var index = value.Span.IndexOf(separator); - return value.SplitAt(index, 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 (ReadOnlyMemory, ReadOnlyMemory?) SplitFirstOfAny(this ReadOnlyMemory value, ReadOnlySpan separators) + { + var index = value.Span.IndexOfAny(separators); + return value.SplitAt(index, 1); + } - 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); - } + 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) + private static (ReadOnlyMemory, ReadOnlyMemory?) SplitAt(this ReadOnlyMemory value, int index, int skip) + { + if (index < 0) { - if (index < 0) - { - return (value, null); - } - - var before = value.Slice(0, index); - var after = value.Slice(index + skip); - return (before, after); + return (value, null); } - private static StringSpanTuple SplitAt(this ReadOnlySpan value, int index, int skip, out bool hasSeparator) + 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) - { - hasSeparator = false; - return new(value, default); - } - - var before = value.Slice(0, index); - var after = value.Slice(index + skip); - hasSeparator = true; - return new(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 82ae61dc..227e5946 100644 --- a/src/Ookii.CommandLine/StringSpan.Async.cs +++ b/src/Ookii.CommandLine/StringSpan.Async.cs @@ -6,73 +6,72 @@ 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, CancellationToken cancellationToken) - { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - await writer.WriteAsync(self, cancellationToken); + 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 0d57fcb8..0bbd211e 100644 --- a/src/Ookii.CommandLine/StringSpanExtensions.cs +++ b/src/Ookii.CommandLine/StringSpanExtensions.cs @@ -4,125 +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); - public delegate bool SplitCallback(ReadOnlySpan 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; + self = remaining; } + } - public static void CopyTo(this ReadOnlySpan self, char[] destination, int start) - { - self.CopyTo(destination.AsSpan(start)); - } + public static partial void WriteTo(this ReadOnlySpan self, TextWriter writer); - public static void Split(this ReadOnlySpan self, ReadOnlySpan separator, SplitCallback callback) + private static (int, int)? BreakLine(ReadOnlySpan span, int startIndex, BreakLineMode mode) + { + switch (mode) { - while (!self.IsEmpty) + case BreakLineMode.Force: + return (startIndex, startIndex); + + case BreakLineMode.Backward: + for (int index = startIndex; index >= 0; --index) { - var (first, remaining) = self.SplitOnce(separator, out bool _); - if (!callback(first)) + if (char.IsWhiteSpace(span[index])) { - break; + return (index, index + 1); } - - self = remaining; } - } - public static partial void WriteTo(this ReadOnlySpan self, TextWriter writer); + break; - private static (int, int)? BreakLine(ReadOnlySpan span, int startIndex, BreakLineMode mode) - { - switch (mode) + case BreakLineMode.Forward: + for (int index = 0; index <= startIndex; ++index) { - case BreakLineMode.Force: - return (startIndex, startIndex); - - case BreakLineMode.Backward: - for (int index = startIndex; index >= 0; --index) - { - if (char.IsWhiteSpace(span[index])) - { - return (index, index + 1); - } - } - - break; - - 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 index 97cb4e8b..a37f93c4 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -1,7 +1,6 @@ using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; namespace Ookii.CommandLine.Support; @@ -104,7 +103,7 @@ public void RunValidators(CommandLineParser parser) } foreach (var validator in _validators) - { + { validator.Validate(parser); } } diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index d7eb12af..6e41eb7b 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -4,10 +4,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Ookii.CommandLine.Support; @@ -173,7 +169,7 @@ protected override void SetProperty(object target, object? value) /// protected override string DetermineValueDescriptionForType(Type type) { - Debug.Assert(type == KeyType || type == ValueType || (ValueType == null && type == ElementType)); + Debug.Assert(type == KeyType || type == ValueType || (ValueType == null && type == ElementType)); if (KeyType != null && type == KeyType) { return _defaultKeyDescription!; diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs index 8c9d4047..803a8e07 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -2,10 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Ookii.CommandLine.Support; diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs index cb9d2456..42c7c5dd 100644 --- a/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs @@ -1,8 +1,8 @@ using Ookii.CommandLine.Commands; using System; -using System.Linq; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; namespace Ookii.CommandLine.Support; diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index 1911c1e5..1dc5ce06 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; -using System.Globalization; +using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Validation; using System; -using System.Reflection; +using System.Collections.Generic; using System.ComponentModel; -using Ookii.CommandLine.Conversion; using System.Diagnostics; -using System.Text; -using Ookii.CommandLine.Validation; -using System.Threading; using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; using System.Runtime.CompilerServices; namespace Ookii.CommandLine.Support; diff --git a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs index 70cf58fe..8aac2a0e 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgumentProvider.cs @@ -6,8 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Ookii.CommandLine.Support; diff --git a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs index b7f44147..7dcdd5b2 100644 --- a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs @@ -7,8 +7,6 @@ using System.Globalization; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Ookii.CommandLine.Support; 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..d88fd449 100644 --- a/src/Ookii.CommandLine/Terminal/TextFormat.cs +++ b/src/Ookii.CommandLine/Terminal/TextFormat.cs @@ -1,191 +1,190 @@ using System; using System.Drawing; -namespace Ookii.CommandLine.Terminal +namespace Ookii.CommandLine.Terminal; + +/// +/// Provides constants for various virtual terminal sequences that control text format. +/// +public static class TextFormat { /// - /// Provides constants for various virtual terminal sequences that control text format. + /// Resets the text format to the settings before modification. /// - public static class TextFormat - { - /// - /// 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"; + 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"; - /// - /// 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) - { - return FormattableString.Invariant($"{VirtualTerminal.Escape}[{(foreground ? 38 : 48)};2;{color.R};{color.G};{color.B}m"); - } + /// + /// 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) + { + return FormattableString.Invariant($"{VirtualTerminal.Escape}[{(foreground ? 38 : 48)};2;{color.R};{color.G};{color.B}m"); } } diff --git a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs index 86a0782f..68264191 100644 --- a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs +++ b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs @@ -1,192 +1,191 @@ 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, - }; - - if (!supported) - { - return new VirtualTerminalSupport(false); - } - - var term = Environment.GetEnvironmentVariable("TERM"); - if (term == "dumb") - { - return new VirtualTerminalSupport(false); - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var previousMode = NativeMethods.EnableVirtualTerminalSequences(stream, true); - if (previousMode == null) - { - return new VirtualTerminalSupport(false); - } + StandardStream.Output => !Console.IsOutputRedirected, + StandardStream.Error => !Console.IsErrorRedirected, + _ => false, + }; - return new VirtualTerminalSupport(NativeMethods.GetStandardHandle(stream), previousMode.Value); - } + if (!supported) + { + return new VirtualTerminalSupport(false); + } - // Support is assumed on non-Windows platforms if TERM is set. - return new VirtualTerminalSupport(term != null); + var term = Environment.GetEnvironmentVariable("TERM"); + if (term == "dumb") + { + return new VirtualTerminalSupport(false); } - /// - /// 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 (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - if (Environment.GetEnvironmentVariable("NO_COLOR") != null) + var previousMode = NativeMethods.EnableVirtualTerminalSequences(stream, true); + if (previousMode == null) { return new VirtualTerminalSupport(false); } - return EnableVirtualTerminalSequences(stream); + return new VirtualTerminalSupport(NativeMethods.GetStandardHandle(stream), previousMode.Value); } - // 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) - { - return -1; - } + // Support is assumed on non-Windows platforms if TERM is set. + return new VirtualTerminalSupport(term != null); + } - 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)), - }; + /// + /// 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); } - 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)), + }; + } + + private static int FindCsiEnd(ReadOnlySpan value, ref StringSegmentType type) + { + int result = FindCsiEndPartial(value.Slice(1), ref type); + return result < 0 ? result : result + 1; + } - ++index; + 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..859ac768 100644 --- a/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs +++ b/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs @@ -1,64 +1,63 @@ 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 { - /// - /// 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; + private readonly bool _supported; + private IntPtr _handle; + private readonly NativeMethods.ConsoleModes _previousMode; - internal VirtualTerminalSupport(bool supported) - { - _supported = supported; - GC.SuppressFinalize(this); - } + internal VirtualTerminalSupport(bool supported) + { + _supported = supported; + GC.SuppressFinalize(this); + } - internal VirtualTerminalSupport(IntPtr handle, NativeMethods.ConsoleModes previousMode) - { - _supported = true; - _handle = handle; - _previousMode = previousMode; - } + internal VirtualTerminalSupport(IntPtr handle, NativeMethods.ConsoleModes previousMode) + { + _supported = true; + _handle = handle; + _previousMode = previousMode; + } - /// - /// Cleans up resources for the class. - /// - ~VirtualTerminalSupport() - { - ResetConsoleMode(); - } + /// + /// Cleans up resources for the class. + /// + ~VirtualTerminalSupport() + { + 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); - } + /// + 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/TypeHelper.cs b/src/Ookii.CommandLine/TypeHelper.cs index 5163d5d9..05fb666a 100644 --- a/src/Ookii.CommandLine/TypeHelper.cs +++ b/src/Ookii.CommandLine/TypeHelper.cs @@ -1,188 +1,186 @@ -// Copyright (c) Sven Groot (Ookii.org) -using Ookii.CommandLine.Conversion; +using Ookii.CommandLine.Conversion; using System; 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"; + private const string ParseMethodName = "Parse"; - public static Type? FindGenericInterface( + public static Type? FindGenericInterface( #if NET6_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] #endif - this Type type, Type interfaceType) + this Type type, Type interfaceType) + { + if (type == null) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - if (interfaceType == null) - { - throw new ArgumentNullException(nameof(interfaceType)); - } + throw new ArgumentNullException(nameof(type)); + } - if (!(interfaceType.IsInterface && interfaceType.IsGenericTypeDefinition)) - { - throw new ArgumentException(Properties.Resources.TypeNotGenericDefinition, nameof(interfaceType)); - } + if (interfaceType == null) + { + throw new ArgumentNullException(nameof(interfaceType)); + } - if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == interfaceType) - { - return type; - } + if (!(interfaceType.IsInterface && interfaceType.IsGenericTypeDefinition)) + { + throw new ArgumentException(Properties.Resources.TypeNotGenericDefinition, nameof(interfaceType)); + } - return type.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType); + if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == interfaceType) + { + return type; } - public static bool ImplementsInterface( + return type.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType); + } + + public static bool ImplementsInterface( #if NET6_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] #endif - this Type type, Type interfaceType) + this Type type, Type interfaceType) + { + if (type == null) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - if (interfaceType == null) - { - throw new ArgumentNullException(nameof(interfaceType)); - } + throw new ArgumentNullException(nameof(type)); + } - return type.GetInterfaces().Any(i => i == interfaceType); + if (interfaceType == null) + { + throw new ArgumentNullException(nameof(interfaceType)); } - public static object? CreateInstance( + return type.GetInterfaces().Any(i => i == interfaceType); + } + + public static object? CreateInstance( #if NET6_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] #endif - this Type type) + this Type type) + { + if (type == null) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - return Activator.CreateInstance(type); + throw new ArgumentNullException(nameof(type)); } - public static object? CreateInstance( + return Activator.CreateInstance(type); + } + + public static object? CreateInstance( #if NET6_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif - this Type type, params object?[]? args) + this Type type, params object?[]? args) + { + if (type == null) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - return Activator.CreateInstance(type, args); + throw new ArgumentNullException(nameof(type)); } + return Activator.CreateInstance(type, args); + } + #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] + [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) + public static ArgumentConverter GetStringConverter(this Type type, Type? converterType) + { + if (type == null) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } + throw new ArgumentNullException(nameof(type)); + } - var converter = (ArgumentConverter?)converterType?.CreateInstance(); - if (converter != null) - { - return converter; - } + var converter = (ArgumentConverter?)converterType?.CreateInstance(); + if (converter != null) + { + return converter; + } - if (converterType == null) + if (converterType == null) + { + var underlyingType = type.IsNullableValueType() ? type.GetGenericArguments()[0] : type; + converter = GetDefaultConverter(underlyingType); + if (converter != null) { - var underlyingType = type.IsNullableValueType() ? type.GetGenericArguments()[0] : type; - converter = GetDefaultConverter(underlyingType); - if (converter != null) - { - return type.IsNullableValueType() - ? new NullableConverter(converter) - : converter; - } + return type.IsNullableValueType() + ? new NullableConverter(converter) + : converter; } - - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoArgumentConverterFormat, type)); } - public static bool IsNullableValueType(this Type type) - { - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - } + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.NoArgumentConverterFormat, type)); + } + + 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; + public static Type GetUnderlyingType(this Type type) + => type.IsNullableValueType() ? type.GetGenericArguments()[0] : type; - private static ArgumentConverter? GetDefaultConverter( + private static ArgumentConverter? GetDefaultConverter( #if NET7_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] #elif NET6_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] #endif - this Type type) + this Type type) + { + if (type == typeof(string)) { - if (type == typeof(string)) - { - return StringConverter.Instance; - } + return StringConverter.Instance; + } - if (type == typeof(bool)) - { - return BooleanConverter.Instance; - } + if (type == typeof(bool)) + { + return BooleanConverter.Instance; + } - if (type.IsEnum) - { - return new EnumConverter(type); - } + if (type.IsEnum) + { + return new EnumConverter(type); + } #if NET7_0_OR_GREATER - if (type.FindGenericInterface(typeof(ISpanParsable<>)) != null) - { - return (ArgumentConverter?)Activator.CreateInstance(typeof(SpanParsableConverter<>).MakeGenericType(type)); - } + if (type.FindGenericInterface(typeof(ISpanParsable<>)) != null) + { + return (ArgumentConverter?)Activator.CreateInstance(typeof(SpanParsableConverter<>).MakeGenericType(type)); + } #endif - // 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); - - if (method != null && method.ReturnType == type) - { - return new ParseConverter(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 ParseConverter(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 ConstructorConverter(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 e066e48d..935001ee 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 - { - /// - /// Only the usage syntax is shown; the argument descriptions are not. In addition, the - /// message is shown. - /// - SyntaxOnly, - /// - /// Full usage help is shown, including the argument descriptions. - /// - Full, - /// - /// 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 e768e487..d29d40af 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -2224,7 +2224,7 @@ private bool CheckShowCommandHelpInstruction() first = false; } - if ((!globalMode && requiredMode != options.Mode) || + if ((!globalMode && requiredMode != options.Mode) || (!globalNameTransform && requiredNameTransform != options.ArgumentNameTransform) || (!globalPrefixes && options.ArgumentNamePrefixes != null) || (actualMode == ParsingMode.LongShort && !globalLongPrefix && options.LongArgumentNamePrefix != null)) diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs index f64eeeb2..14da848a 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs @@ -1,214 +1,213 @@ 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, 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. +/// +/// +/// +[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 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)); - } - - if (!IsValid(argument, value)) - { - throw new CommandLineArgumentException(GetErrorMessage(argument, value), argument.ArgumentName, ErrorCategory); - } + throw new ArgumentNullException(nameof(argument)); } - /// - /// 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. - /// - /// - /// - /// The class will only call this method if the - /// property is . - /// - /// - /// - /// The parameter is not a valid value. The - /// property will be the value of the property. - /// - public bool ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) + var result = IsSpanValid(argument, value); + if (result == false) { - if (argument == null) - { - throw new ArgumentNullException(nameof(argument)); - } - - var result = IsSpanValid(argument, value); - if (result == false) - { - throw new CommandLineArgumentException(GetErrorMessage(argument, value.ToString()), argument.ArgumentName, ErrorCategory); - } - - return result != null; + throw new CommandLineArgumentException(GetErrorMessage(argument, value.ToString()), argument.ArgumentName, ErrorCategory); } + return result != 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 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); - /// - /// 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; + /// + /// 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 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); - /// - /// 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); + /// + /// 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 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; - } + /// + /// 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; } diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs index eb593a47..455f5239 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs @@ -1,62 +1,61 @@ -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 is 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 is 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..f5db0d48 100644 --- a/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs @@ -1,104 +1,103 @@ 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, 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 { /// - /// 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 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(); - /// - /// 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 argument 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 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; } diff --git a/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs b/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs index b204bf7f..71894bcd 100644 --- a/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs @@ -4,136 +4,135 @@ 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 . + /// + public 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 . + /// + public 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 + /// 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 (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..9fb26097 100644 --- a/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs @@ -1,67 +1,66 @@ using System; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates that an argument cannot 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 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 . +/// +/// +/// 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. +/// +/// +/// +public class ProhibitsAttribute : DependencyValidationAttribute { /// - /// Validates that an argument cannot be used together with other arguments. + /// Initializes a new instance of the class. /// - /// - /// - /// 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 . - /// - /// - /// 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. - /// - /// - /// - public class ProhibitsAttribute : DependencyValidationAttribute + /// The name of the argument that this argument prohibits. + /// + /// is . + /// + public ProhibitsAttribute(string argument) + : base(false, argument) { - /// - /// 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) - { - } + /// + /// 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)); + /// + /// 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..791e8024 100644 --- a/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs @@ -3,200 +3,199 @@ 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 . +/// +/// +/// 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. +/// +/// +/// +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 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 (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..2c6fc0c1 100644 --- a/src/Ookii.CommandLine/Validation/RequiresAttribute.cs +++ b/src/Ookii.CommandLine/Validation/RequiresAttribute.cs @@ -1,67 +1,66 @@ 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 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 . +/// +/// +/// 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. +/// +/// +/// +public class RequiresAttribute : DependencyValidationAttribute { /// - /// Validates that an argument can only be used together with other arguments. + /// Initializes a new instance of the class. /// - /// - /// - /// 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 . - /// - /// - /// 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. - /// - /// - /// - public class RequiresAttribute : DependencyValidationAttribute + /// 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. - /// - /// 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) - { - } + /// + /// 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)); + /// + /// 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..3e416faa 100644 --- a/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs @@ -1,99 +1,98 @@ 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 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 { + 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 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 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.IsMultiValue) { - 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. + 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); } diff --git a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs index 9af9e99b..8534bdb8 100644 --- a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs @@ -1,83 +1,82 @@ -using System; +using Ookii.CommandLine.Conversion; +using System; using System.Globalization; -using Ookii.CommandLine.Conversion; -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 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. +/// +/// +/// 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 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. - /// - /// - /// 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. + /// 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 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); - } + public bool IncludeValuesInErrorMessage { get; set; } - /// - /// Gets or sets a value that indicates whether the possible values of the enumeration - /// should be included in the error message if validation fails. - /// - /// - /// to include the values; otherwise, . - /// - /// - /// - /// 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; } + /// + protected override string GetUsageHelpCore(CommandLineArgument argument) + => argument.Parser.StringProvider.ValidateEnumValueUsageHelp(argument.ElementType); - /// - 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 a75c0769..1e2ece37 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs @@ -1,71 +1,70 @@ using System; -namespace Ookii.CommandLine.Validation +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 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. + /// + /// + /// . + /// + public override ValidationMode Mode => ValidationMode.BeforeConversion; + + /// + /// Determines if the argument is valid. /// - /// - /// - /// 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 + /// 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 is valid. - /// - /// 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 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); } - - /// - public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) - => !value.IsEmpty; - - /// - /// 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 c3ac690f..4561825b 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs @@ -2,61 +2,60 @@ 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 null. /// - /// - /// - /// 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 + /// 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 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; - } + 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); - } + /// + /// 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); } } diff --git a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs index b3324c91..228a5e5d 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs @@ -1,74 +1,73 @@ using System; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + + +/// +/// Validates that the value of an argument is not an empty string, or a string containing only +/// white-space characters. +/// +/// +/// +/// 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 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 null or only white-space characters. /// - /// - /// - /// 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 ValidateNotWhiteSpaceAttribute : ArgumentValidationWithHelpAttribute + /// The argument being validated. + /// + /// The raw string argument value. + /// + /// + /// 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.IsNullOrWhiteSpace(value as string); + } + + /// + public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) + => !value.IsWhiteSpace(); - /// - /// Determines if the argument's value is not null or 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) + /// + /// 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.IsNullOrWhiteSpace(value as string); + return argument.Parser.StringProvider.NullArgumentValue(argument.ArgumentName); } - - /// - 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. - 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(); + /// + 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 74c4afb9..981a4e53 100644 --- a/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs @@ -2,133 +2,132 @@ using System.Globalization; using System.Text.RegularExpressions; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates that an argument's value matches the specified . +/// +/// +/// +/// 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. +/// +/// +/// +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(string pattern, RegexOptions options = RegexOptions.None) { - private readonly string _pattern; - private Regex? _patternRegex; - private readonly RegexOptions _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; - } + _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. - /// - /// - 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 pattern that values must match. - /// - /// - /// The pattern that values must match. - /// - public 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); - /// - /// 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) + /// + /// 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; } + return Pattern.IsMatch(stringValue); + } + #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); + /// + /// 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) + /// + /// 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) { - if (ErrorMessage == null) - { - return base.GetErrorMessage(argument, value); - } - - return string.Format(CultureInfo.CurrentCulture, ErrorMessage, argument.ArgumentName, value, _pattern); + 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 33b375c9..73759bde 100644 --- a/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs @@ -1,111 +1,109 @@ 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 string. - /// - /// - /// - /// 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. + public override string GetErrorMessage(CommandLineArgument argument, object? value) + => argument.Parser.StringProvider.ValidateRangeFailed(argument.ArgumentName, this); + + /// + 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 f5587d6b..b2b32dde 100644 --- a/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs @@ -1,91 +1,90 @@ using System; -namespace Ookii.CommandLine.Validation +namespace Ookii.CommandLine.Validation; + +/// +/// Validates that the string length of an argument's value is in the specified range. +/// +/// +/// +/// 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 { + 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; - - /// - /// 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; - } + _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 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; + } - /// - public override bool? IsSpanValid(CommandLineArgument argument, ReadOnlySpan value) - { - var length = value.Length; - 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. + 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); - } + /// + 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..4e121000 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 derived class of 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/WrappingMode.cs b/src/Ookii.CommandLine/WrappingMode.cs index 21b9f9bd..9c42e0bf 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 } From 4fc645f4f55388d3909039d869b4077395d6824d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 13:40:35 -0700 Subject: [PATCH 171/234] Enable nullable for test project. --- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 5 + .../ArgumentValidatorTest.cs | 140 ++++++------- .../CommandLineParserNullableTest.cs | 5 - .../CommandLineParserTest.cs | 187 +++++++++--------- src/Ookii.CommandLine.Tests/CommandTypes.cs | 4 +- .../KeyValuePairConverterTest.cs | 4 +- .../LineWrappingTextWriterTest.cs | 10 +- .../NetStandardHelpers.cs | 71 ++++--- .../Ookii.CommandLine.Tests.csproj | 2 +- .../ParseOptionsTest.cs | 2 +- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 30 +-- 11 files changed, 233 insertions(+), 227 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 7c6e174b..1260647a 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -8,6 +8,11 @@ using System.IO; using System.Net; +// 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 diff --git a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs index b169e15d..f711c3b8 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs @@ -11,134 +11,140 @@ namespace Ookii.CommandLine.Tests; [TestClass] public class ArgumentValidatorTest { - 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 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)); + 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)); + 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)); + 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)); + 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, "")); + 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, "")); + 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)); + 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)); + 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)); + 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)); + 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.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 parser = new CommandLineParser(); var validator = new ValidateEnumValueAttribute(); - var argument = _parser.GetArgument("Day"); + 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"); + 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() + { + // 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 bbcf8f51..90319438 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserNullableTest.cs @@ -1,14 +1,9 @@ // These tests don't apply to .Net Standard. #if NET6_0_OR_GREATER -#nullable enable using Microsoft.VisualStudio.TestTools.UnitTesting; -using Ookii.CommandLine.Conversion; using Ookii.CommandLine.Support; -using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; using System.Reflection; namespace Ookii.CommandLine.Tests; diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index f2433a82..233c7ca1 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -185,7 +185,7 @@ public void ParseTestDuplicateDictionaryKeys(ProviderKind kind) { var target = CreateParser(kind); - DictionaryArguments args = target.Parse(new[] { "-DuplicateKeys", "Foo=1", "-DuplicateKeys", "Bar=2", "-DuplicateKeys", "Foo=3" }); + 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"]); @@ -205,7 +205,7 @@ public void ParseTestMultiValueSeparator(ProviderKind kind) { var target = CreateParser(kind); - MultiValueSeparatorArguments args = target.Parse(new[] { "-NoSeparator", "Value1,Value2", "-NoSeparator", "Value3", "-Separator", "Value1,Value2", "-Separator", "Value3" }); + 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); @@ -266,7 +266,7 @@ public void ParseTestKeyValueSeparator(ProviderKind kind) 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<=>" }); + 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, @@ -546,17 +546,17 @@ public void TestCancelParsing(ProviderKind kind) 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); + 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) + static void handler1(object? sender, ArgumentParsedEventArgs e) { if (e.Argument.ArgumentName == "DoesNotCancel") { @@ -572,18 +572,18 @@ static void handler1(object sender, ArgumentParsedEventArgs e) 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); + 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) + static void handler2(object? sender, ArgumentParsedEventArgs e) { if (e.Argument.ArgumentName == "DoesCancel") { @@ -723,13 +723,13 @@ public void TestLongShortMode(ProviderKind kind) 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); + 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" }); + 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); @@ -739,13 +739,13 @@ public void TestLongShortMode(ProviderKind kind) Assert.IsFalse(result.Switch3); // Combine switches. - result = parser.Parse(new[] { "-su" }); + result = CheckSuccess(parser, new[] { "-su" }); Assert.IsTrue(result.Switch1); Assert.IsFalse(result.Switch2); Assert.IsTrue(result.Switch3); // Use a short alias. - result = parser.Parse(new[] { "-b", "5" }); + result = CheckSuccess(parser, new[] { "-b", "5" }); Assert.AreEqual(5, result.Arg2); // Combining non-switches is an error. @@ -767,7 +767,7 @@ public void TestMethodArguments(ProviderKind kind) { var parser = CreateParser(kind); - Assert.AreEqual(ArgumentKind.Method, parser.GetArgument("NoCancel").Kind); + Assert.AreEqual(ArgumentKind.Method, parser.GetArgument("NoCancel")!.Kind); Assert.IsNull(parser.GetArgument("NotAnArgument")); Assert.IsNull(parser.GetArgument("NotStatic")); Assert.IsNull(parser.GetArgument("NotPublic")); @@ -959,15 +959,15 @@ public void TestValidation(ProviderKind kind) // Range validator on property CheckThrows(parser, new[] { "-Arg1", "0" }, CommandLineArgumentErrorCategory.ValidationFailed, "Arg1", remainingArgumentCount: 2); - var result = parser.Parse(new[] { "-Arg1", "1" }); + var result = CheckSuccess(parser, new[] { "-Arg1", "1" }); Assert.AreEqual(1, result.Arg1); - result = parser.Parse(new[] { "-Arg1", "5" }); + 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 = parser.Parse(new[] { " " }); + result = CheckSuccess(parser, new[] { " " }); Assert.AreEqual(" ", result.Arg2); // Multiple validators on method @@ -978,35 +978,35 @@ public void TestValidation(ProviderKind kind) 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); - parser.Parse(new[] { "-Arg3", "1023" }); + 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 = parser.Parse(new[] { "-Arg4", "foo;bar" }); + result = CheckSuccess(parser, new[] { "-Arg4", "foo;bar" }); CollectionAssert.AreEqual(new[] { "foo", "bar" }, result.Arg4); - result = parser.Parse(new[] { "-Arg4", "foo", "-Arg4", "bar" }); + 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 = parser.Parse(new[] { "-Arg4", "foo;bar;baz;ban" }); + 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 = parser.Parse(new[] { "-Day", "1" }); + 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 = parser.Parse(new[] { "-Day2", "1" }); + result = CheckSuccess(parser, new[] { "-Day2", "1" }); Assert.AreEqual(DayOfWeek.Monday, result.Day2); - result = parser.Parse(new[] { "-Day2", "" }); + result = CheckSuccess(parser, new[] { "-Day2", "" }); Assert.IsNull(result.Day2); // NotNull validator with Nullable. @@ -1020,16 +1020,16 @@ public void TestRequires(ProviderKind kind) var parser = CreateParser(kind); // None of these have remaining arguments because validation happens after parsing. - var result = parser.Parse(new[] { "-Address", "127.0.0.1" }); + 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 = parser.Parse(new[] { "-Address", "127.0.0.1", "-Port", "9000" }); + 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 = parser.Parse(new[] { "-Protocol", "1", "-Address", "127.0.0.1", "-Throughput", "10" }); + 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); @@ -1041,7 +1041,7 @@ public void TestProhibits(ProviderKind kind) { var parser = CreateParser(kind); - var result = parser.Parse(new[] { "-Path", "test" }); + 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"); @@ -1090,9 +1090,9 @@ public void TestDefaultValueDescriptions(ProviderKind kind) }; 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); + Assert.AreEqual("Switch", parser.GetArgument("Arg7")!.ValueDescription); + Assert.AreEqual("Number", parser.GetArgument("Arg9")!.ValueDescription); + Assert.AreEqual("String=Number", parser.GetArgument("Arg13")!.ValueDescription); } [TestMethod] @@ -1100,17 +1100,17 @@ public void TestDefaultValueDescriptions(ProviderKind kind) public void TestMultiValueWhiteSpaceSeparator(ProviderKind kind) { var parser = CreateParser(kind); - Assert.IsTrue(parser.GetArgument("Multi").AllowMultiValueWhiteSpaceSeparator); - Assert.IsFalse(parser.GetArgument("MultiSwitch").AllowMultiValueWhiteSpaceSeparator); - Assert.IsFalse(parser.GetArgument("Other").AllowMultiValueWhiteSpaceSeparator); + 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" }); + 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 = parser.Parse(new[] { "-Multi", "1", "-Multi", "2" }); + 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); @@ -1124,7 +1124,7 @@ public void TestMultiValueWhiteSpaceSeparator(ProviderKind kind) public void TestInjection(ProviderKind kind) { var parser = CreateParser(kind); - var result = parser.Parse(new[] { "-Arg", "1" }); + var result = CheckSuccess(parser, new[] { "-Arg", "1" }); Assert.AreSame(parser, result.Parser); Assert.AreEqual(1, result.Arg); } @@ -1136,7 +1136,7 @@ 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 = parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }); + var result = CheckSuccess(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }); Assert.AreEqual("bar", result.Argument1); bool handlerCalled = false; @@ -1162,7 +1162,7 @@ public void TestDuplicateArguments(ProviderKind kind) // Now it is called. parser.Options.DuplicateArguments = ErrorMode.Allow; - result = parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }); + result = CheckSuccess(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }); Assert.AreEqual("bar", result.Argument1); Assert.IsTrue(handlerCalled); @@ -1170,7 +1170,7 @@ public void TestDuplicateArguments(ProviderKind kind) parser.Options.DuplicateArguments = ErrorMode.Warning; handlerCalled = false; keepOldValue = true; - result = parser.Parse(new[] { "-Argument1", "foo", "-Argument1", "bar" }); + result = CheckSuccess(parser, new[] { "-Argument1", "foo", "-Argument1", "bar" }); Assert.AreEqual("foo", result.Argument1); Assert.IsTrue(handlerCalled); } @@ -1180,27 +1180,27 @@ public void TestDuplicateArguments(ProviderKind kind) public void TestConversion(ProviderKind kind) { var parser = CreateParser(kind); - var result = parser.Parse("-ParseCulture 1 -ParseStruct 2 -Ctor 3 -ParseNullable 4 -ParseMulti 5 6 -ParseNullableMulti 7 8 -NullableMulti 9 10 -Nullable 11".Split(' ')); + 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(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(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" }); + result = CheckSuccess(parser, new[] { "-ParseNullable", "", "-NullableMulti", "1", "", "2", "-ParseNullableMulti", "3", "", "4" }); Assert.IsNull(result.ParseNullable); - Assert.AreEqual(1, result.NullableMulti[0].Value); + 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); + 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); } [TestMethod] @@ -1223,18 +1223,18 @@ public void TestDerivedClass(ProviderKind kind) 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); + 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); + Assert.IsNull(parser.GetArgument("Arg6")!.DefaultValue); + Assert.AreEqual(0, parser.GetArgument("Arg7")!.DefaultValue); // Null because set to "default". - Assert.IsNull(parser.GetArgument("Arg8").DefaultValue); + Assert.IsNull(parser.GetArgument("Arg8")!.DefaultValue); // Null because explicit null. - Assert.IsNull(parser.GetArgument("Arg9").DefaultValue); + Assert.IsNull(parser.GetArgument("Arg9")!.DefaultValue); } [TestMethod] @@ -1318,24 +1318,25 @@ public ExpectedArgument(string name, Type type, ArgumentKind kind = ArgumentKind } public string Name { get; set; } - public string MemberName { get; set; } + public string? MemberName { get; set; } public Type Type { get; set; } - public Type ElementType { 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 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 string[]? Aliases { get; set; } public char? ShortName { get; set; } - public char[] ShortAliases { get; set; } + public char[]? ShortAliases { get; set; } public bool IsHidden { get; set; } } - private static void VerifyArgument(CommandLineArgument argument, ExpectedArgument expected) + 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); @@ -1372,7 +1373,7 @@ private static void VerifyArguments(IEnumerable arguments, 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 = 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) + 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) { string[] args = commandLine.Split(' '); // not using quoted arguments in the tests, so this is fine. var result = target.Parse(args); @@ -1423,7 +1424,7 @@ private static void TestParse(CommandLineParser target, string co } } - private static void CheckThrows(CommandLineParser parser, string[] arguments, CommandLineArgumentErrorCategory category, string argumentName = null, Type innerExceptionType = null, int remainingArgumentCount = 0) + private static void CheckThrows(CommandLineParser parser, string[] arguments, CommandLineArgumentErrorCategory category, string? argumentName = null, Type? innerExceptionType = null, int remainingArgumentCount = 0) { try { @@ -1435,7 +1436,7 @@ private static void CheckThrows(CommandLineParser parser, string[] arguments, Co 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(ex.ArgumentName, parser.ParseResult.LastException!.ArgumentName); Assert.AreEqual(category, ex.Category); Assert.AreEqual(argumentName, ex.ArgumentName); if (innerExceptionType == null) @@ -1463,7 +1464,7 @@ private static void CheckCanceled(CommandLineParser parser, string[] arguments, AssertMemoryEqual(remaining, parser.ParseResult.RemainingArguments); } - private static T CheckSuccess(CommandLineParser parser, string[] arguments, string argumentName = null, int remainingArgumentCount = 0) + private static T CheckSuccess(CommandLineParser parser, string[] arguments, string? argumentName = null, int remainingArgumentCount = 0) where T : class { var result = parser.Parse(arguments); @@ -1477,7 +1478,7 @@ private static T CheckSuccess(CommandLineParser parser, string[] arguments return result; } - internal static CommandLineParser CreateParser(ProviderKind kind, ParseOptions options = null) + internal static CommandLineParser CreateParser(ProviderKind kind, ParseOptions? options = null) #if NET7_0_OR_GREATER where T : class, IParserProvider #else @@ -1490,7 +1491,7 @@ internal static CommandLineParser CreateParser(ProviderKind kind, ParseOpt #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 }), + ProviderKind.Generated => (CommandLineParser)typeof(T).InvokeMember("CreateParser", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object?[] { options })!, #endif _ => throw new InvalidOperationException() }; @@ -1499,7 +1500,7 @@ internal static CommandLineParser CreateParser(ProviderKind kind, ParseOpt return parser; } - private static T StaticParse(ProviderKind kind, string[] args, ParseOptions options = null) + private static T? StaticParse(ProviderKind kind, string[] args, ParseOptions? options = null) #if NET7_0_OR_GREATER where T : class, IParser #else @@ -1512,7 +1513,7 @@ private static T StaticParse(ProviderKind kind, string[] args, ParseOptions o #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 }), + ProviderKind.Generated => (T?)typeof(T).InvokeMember("Parse", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object?[] { args, options }), #endif _ => throw new InvalidOperationException() }; diff --git a/src/Ookii.CommandLine.Tests/CommandTypes.cs b/src/Ookii.CommandLine.Tests/CommandTypes.cs index 6a7f23b6..9cee1d74 100644 --- a/src/Ookii.CommandLine.Tests/CommandTypes.cs +++ b/src/Ookii.CommandLine.Tests/CommandTypes.cs @@ -23,7 +23,7 @@ partial class GeneratedManagerWithMultipleAssemblies { } public partial class TestCommand : ICommand { [CommandLineArgument] - public string Argument { get; set; } + public string? Argument { get; set; } public int Run() { @@ -55,7 +55,7 @@ public void Parse(ReadOnlyMemory args, CommandManager manager) Value = args.Span[0]; } - public string Value { get; set; } + public string? Value { get; set; } public int Run() { diff --git a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs index 9d386b2b..126995fa 100644 --- a/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs +++ b/src/Ookii.CommandLine.Tests/KeyValuePairConverterTest.cs @@ -20,7 +20,7 @@ public void TestConvertFrom() { var parser = new CommandLineParser(); var converter = new KeyValuePairConverter(); - var converted = converter.Convert("foo=5", CultureInfo.InvariantCulture, parser.GetArgument("Argument1")); + var converted = converter.Convert("foo=5", CultureInfo.InvariantCulture, parser.GetArgument("Argument1")!); Assert.AreEqual(KeyValuePair.Create("foo", 5), converted); } @@ -29,7 +29,7 @@ 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")); + 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.cs b/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs index be3aab67..6ae81a9e 100644 --- a/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs +++ b/src/Ookii.CommandLine.Tests/LineWrappingTextWriterTest.cs @@ -200,7 +200,7 @@ public void TestConstructor() [ExpectedException(typeof(ArgumentNullException))] public void ConstructorTestBaseWriterNull() { - new LineWrappingTextWriter(null, 0, false); + new LineWrappingTextWriter(null!, 0, false); } [TestMethod()] @@ -497,7 +497,7 @@ private static string WriteString(LineWrappingTextWriter writer, string value, i } writer.Flush(); - return writer.ToString(); + return writer.ToString()!; } private static async Task WriteStringAsync(string value, int maxLength, int segmentSize, int indent = 0) @@ -516,7 +516,7 @@ private static async Task WriteStringAsync(LineWrappingTextWriter writer } await writer.FlushAsync(); - return writer.ToString(); + return writer.ToString()!; } @@ -530,7 +530,7 @@ private static string WriteCharArray(char[] value, int maxLength, int segmentSiz } writer.Flush(); - return writer.BaseWriter.ToString(); + return writer.BaseWriter.ToString()!; } private static string WriteChars(char[] value, int maxLength, int indent = 0) @@ -543,6 +543,6 @@ private static string WriteChars(char[] value, int maxLength, int indent = 0) } writer.Flush(); - return writer.BaseWriter.ToString(); + return writer.BaseWriter.ToString()!; } } 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/Ookii.CommandLine.Tests.csproj b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj index 8c671298..4d295e4d 100644 --- a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj +++ b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj @@ -2,7 +2,7 @@ net7.0;net6.0;net48 - disable + enable Ookii.CommandLine Unit Tests Tests for Ookii.CommandLine. false diff --git a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs index ca7593a4..ad820317 100644 --- a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs +++ b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs @@ -123,7 +123,7 @@ public void TestMerge() options.Merge(attribute); Assert.AreEqual(StringComparison.InvariantCulture, options.ArgumentNameComparison); - CollectionAssert.AreEqual(new[] { "+", "++" }, options.ArgumentNamePrefixes.ToArray()); + CollectionAssert.AreEqual(new[] { "+", "++" }, options.ArgumentNamePrefixes!.ToArray()); Assert.AreEqual("+++", options.LongArgumentNamePrefix); options = new ParseOptions(); diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index 5a074013..a9e747eb 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -103,25 +103,25 @@ public void CreateCommandTest(ProviderKind kind) }; var manager = CreateManager(kind, options); - TestCommand command = (TestCommand)manager.CreateCommand("test", new[] { "-Argument", "Foo" }, 0); + var 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" }); + 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); + 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" }); + var command3 = (CustomParsingCommand?)manager.CreateCommand(new[] { "custom", "hello" }); Assert.IsNotNull(command3); // None because of custom parsing. Assert.AreEqual(ParseStatus.None, manager.ParseResult.Status); @@ -143,7 +143,7 @@ public void CreateCommandTest(ProviderKind kind) 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(CommandLineArgumentErrorCategory.UnknownArgument, manager.ParseResult.LastException!.Category); Assert.AreEqual(manager.ParseResult.ArgumentName, manager.ParseResult.LastException.ArgumentName); Assert.AreNotEqual("", writer.BaseWriter.ToString()); @@ -473,12 +473,12 @@ public void TestAutoPrefixAliases(ProviderKind kind) Assert.IsNull(manager.GetCommand("tes")); // Not ambiguous - Assert.AreEqual("TestParentCommand", manager.GetCommand("testp").Name); - Assert.AreEqual("version", manager.GetCommand("v").Name); + Assert.AreEqual("TestParentCommand", manager.GetCommand("testp")!.Name); + Assert.AreEqual("version", manager.GetCommand("v")!.Name); // Case sensitive, "tes" is no longer ambigous. manager = CreateManager(kind, new CommandOptions() { CommandNameComparison = StringComparison.Ordinal }); - Assert.AreEqual("test", manager.GetCommand("tes").Name); + Assert.AreEqual("test", manager.GetCommand("tes")!.Name); } private class VersionCommandStringProvider : LocalizedStringProvider @@ -502,18 +502,18 @@ public void TestVersionCommandConflict(ProviderKind kind) Assert.IsNull(manager.GetCommand("version")); // Name returns our command. - Assert.AreEqual(typeof(AnotherSimpleCommand), manager.GetCommand("AnotherSimpleCommand").CommandType); + Assert.AreEqual(typeof(AnotherSimpleCommand), manager.GetCommand("AnotherSimpleCommand")!.CommandType); // There is only one in the list of commands. Assert.AreEqual(1, manager.GetCommands().Where(c => c.Name == "AnotherSimpleCommand").Count()); // Prefix is not ambiguous because the automatic command doesn't exist. - Assert.AreEqual(typeof(AnotherSimpleCommand), manager.GetCommand("Another").CommandType); + Assert.AreEqual(typeof(AnotherSimpleCommand), manager.GetCommand("Another")!.CommandType); // 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").SingleOrDefault().CommandType); + Assert.AreEqual(typeof(AutomaticVersionCommand), manager.GetCommand("AnotherSimpleCommand")!.CommandType); + Assert.AreEqual(typeof(AutomaticVersionCommand), manager.GetCommands().Where(c => c.Name == "AnotherSimpleCommand").Single().CommandType); } [TestMethod] @@ -543,13 +543,13 @@ public void TestNoVersionCommand(ProviderKind kind) Assert.IsFalse(manager.GetCommands().Any(c => c.Name == "version")); } - private record struct ExpectedCommand(string Name, Type Type, bool CustomParsing = false, params string[] Aliases) + private record struct ExpectedCommand(string Name, Type? Type, bool CustomParsing = false, params string[]? Aliases) { public Type ParentCommand { get; set; } } - private static void VerifyCommand(CommandInfo command, string name, Type type, bool customParsing = false, string[] aliases = null) + private static void VerifyCommand(CommandInfo command, string name, Type? type, bool customParsing = false, string[]? aliases = null) { Assert.AreEqual(name, command.Name); if (type != null) @@ -575,7 +575,7 @@ private static void VerifyCommands(IEnumerable actual, params Expec } - public static CommandManager CreateManager(ProviderKind kind, CommandOptions options = null) + public static CommandManager CreateManager(ProviderKind kind, CommandOptions? options = null) { var manager = kind switch { From 2be7a8c9fdf5c4c7f9b5f29fd6c572c8fc226bd1 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 15:04:07 -0700 Subject: [PATCH 172/234] Update parser samples. --- .../ArgumentDependencies.csproj | 9 ++- src/Samples/ArgumentDependencies/Program.cs | 41 +++++------ .../ArgumentDependencies/ProgramArguments.cs | 14 ++-- src/Samples/ArgumentDependencies/README.md | 12 ++-- src/Samples/CustomUsage/CustomUsage.csproj | 11 ++- src/Samples/CustomUsage/Program.cs | 20 ++++-- src/Samples/CustomUsage/ProgramArguments.cs | 40 +++-------- src/Samples/CustomUsage/README.md | 5 +- src/Samples/LongShort/LongShort.csproj | 11 ++- src/Samples/LongShort/Program.cs | 66 ++++++++--------- src/Samples/LongShort/ProgramArguments.cs | 53 +++++--------- src/Samples/LongShort/README.md | 16 ++++- src/Samples/Parser/Parser.csproj | 11 ++- src/Samples/Parser/Program.cs | 72 +++++++++++-------- src/Samples/Parser/ProgramArguments.cs | 71 ++++++++---------- src/Samples/Parser/README.md | 32 ++++----- src/Samples/Wpf/App.xaml.cs | 7 +- src/Samples/Wpf/Arguments.cs | 19 ++--- src/Samples/Wpf/README.md | 18 ++--- src/Samples/Wpf/Wpf.csproj | 9 ++- 20 files changed, 272 insertions(+), 265 deletions(-) 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..42525a8c 100644 --- a/src/Samples/ArgumentDependencies/README.md +++ b/src/Samples/ArgumentDependencies/README.md @@ -47,9 +47,9 @@ validators like [`ValidateRangeAttribute`][]), and all the included validators c case-by-case basis with the [`IncludeInUsageHelp`][IncludeInUsageHelp_0] property on each validator attribute. -[`ProhibitsAttribute`]: https://www.ookii.org/docs/commandline-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/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..f44d102a 100644 --- a/src/Samples/LongShort/README.md +++ b/src/Samples/LongShort/README.md @@ -1,10 +1,20 @@ -# 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. +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. @@ -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/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 29f34105..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,26 +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, 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(Position = 2, DefaultValue = 1)] + [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". @@ -59,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] @@ -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] 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 0b796e79..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,20 +45,20 @@ 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.")] 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. - + + + From 8afe539fec2e2ee2b2e8537ac10c2ce73aa7a3df Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 15:36:51 -0700 Subject: [PATCH 173/234] Exclude individual argument default values from the usage help. --- docs/ChangeLog.md | 7 ++-- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 3 +- .../CommandLineParserTest.cs | 4 +-- src/Ookii.CommandLine/CommandLineArgument.cs | 26 +++++++++++++- .../CommandLineArgumentAttribute.cs | 34 +++++++++++++++++-- src/Ookii.CommandLine/UsageWriter.cs | 2 +- 6 files changed, 67 insertions(+), 9 deletions(-) diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index 6ebce54a..285db7d4 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -37,13 +37,15 @@ please check the [migration guide](Migrating.md). success. - The remaining unparsed arguments, if parsing was canceled or encountered an error, are available through the [`CommandLineParser.ParseResult`][] property. -- By default, only usage syntax is shown if a parsing error occurs; the help argument must be used - to get full help. - 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. - Various bug fixes and minor improvements. ## Ookii.CommandLine 3.1.1 @@ -212,6 +214,7 @@ may require substantial code changes and may change how command lines are parsed [`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm [`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm [`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 diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 1260647a..c2011aeb 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -54,7 +54,8 @@ partial class TestArguments public int Arg4 { get; set; } // Short/long name stuff should be ignored if not using LongShort mode. - [CommandLineArgument(Position = 4, ShortName = 'a', IsLong = false), Description("Arg5 description.")] + [CommandLineArgument(Position = 4, ShortName = 'a', IsLong = false, DefaultValue = 1.0f, IncludeDefaultInUsageHelp = false)] + [Description("Arg5 description.")] public float Arg5 { get; set; } [Alias("Alias1")] diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 233c7ca1..58ffdf8a 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -71,7 +71,7 @@ public void ConstructorTest(ProviderKind kind) 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." }, + 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" } }, @@ -1373,7 +1373,7 @@ private static void VerifyArguments(IEnumerable arguments, 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 = 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) + 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); diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index f0d419c3..533eb1bd 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -289,6 +289,7 @@ internal struct ArgumentInfo 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 string? MultiValueSeparator { get; set; } @@ -393,6 +394,7 @@ internal CommandLineArgument(ArgumentInfo info) Position = info.Position; _converter = info.Converter; _defaultValue = ConvertToArgumentTypeInvariant(info.DefaultValue); + IncludeDefaultInUsageHelp = info.IncludeDefaultValueInHelp; _valueDescription = info.ValueDescription; _allowDuplicateDictionaryKeys = info.AllowDuplicateDictionaryKeys; _allowMultiValueWhiteSpaceSeparator = IsMultiValue && !IsSwitch && info.AllowMultiValueWhiteSpaceSeparator; @@ -704,6 +706,27 @@ public object? DefaultValue get { return _defaultValue; } } + /// + /// 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; } + /// /// Gets the description of the argument. /// @@ -1220,6 +1243,7 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, Aliases = GetAliases(aliasAttributes, argumentName), ShortAliases = GetShortAliases(shortAliasAttributes, argumentName), DefaultValue = attribute.DefaultValue, + IncludeDefaultValueInHelp = attribute.IncludeDefaultInUsageHelp, IsRequired = attribute.IsRequired || requiredProperty, IsRequiredProperty = requiredProperty, MemberName = memberName, @@ -1353,7 +1377,7 @@ internal bool HasInformation(UsageWriter writer) return true; } - if (writer.IncludeDefaultValueInDescription && DefaultValue != null) + if (writer.IncludeDefaultValueInDescription && IncludeDefaultInUsageHelp && DefaultValue != null) { return true; } diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index bb8d6f04..b9dd46c8 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -267,13 +267,43 @@ public bool IsPositional /// /// /// By default, the command line usage help generated by - /// includes the default value. To change that, set the - /// property to . + /// 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 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 , 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. diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index d29d40af..dfbdc6b1 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -1404,7 +1404,7 @@ protected virtual void WriteArgumentDescriptionBody(CommandLineArgument argument WriteArgumentValidators(argument); } - if (IncludeDefaultValueInDescription && argument.DefaultValue != null) + if (IncludeDefaultValueInDescription && argument.IncludeDefaultInUsageHelp && argument.DefaultValue != null) { WriteDefaultValue(argument.DefaultValue); } From d8df79ad278a0f577217938d1b895fb0c3efd9b5 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 16:40:02 -0700 Subject: [PATCH 174/234] Update subcommand samples. --- .../CommandGenerator.cs | 2 +- src/Samples/NestedCommands/BaseCommand.cs | 4 +- src/Samples/NestedCommands/CourseCommands.cs | 16 ++-- .../NestedCommands/GeneratedManager.cs | 1 + src/Samples/NestedCommands/ListCommand.cs | 2 +- .../NestedCommands/NestedCommands.csproj | 5 +- src/Samples/NestedCommands/Program.cs | 6 +- src/Samples/NestedCommands/README.md | 56 ++++++------ src/Samples/NestedCommands/StudentCommands.cs | 32 +++---- src/Samples/Subcommand/EncodingConverter.cs | 4 +- src/Samples/Subcommand/GeneratedManager.cs | 10 +++ src/Samples/Subcommand/Program.cs | 9 +- src/Samples/Subcommand/README.md | 19 +++-- src/Samples/Subcommand/ReadCommand.cs | 25 ++---- src/Samples/Subcommand/Subcommand.csproj | 10 ++- src/Samples/Subcommand/WriteCommand.cs | 19 ++--- .../TopLevelArguments/CommandUsageWriter.cs | 5 -- .../TopLevelArguments/EncodingConverter.cs | 3 +- src/Samples/TopLevelArguments/Program.cs | 20 +---- src/Samples/TopLevelArguments/README.md | 85 ++++++++++++++++++- src/Samples/TopLevelArguments/ReadCommand.cs | 13 +-- .../TopLevelArguments/TopLevelArguments.cs | 8 +- .../TopLevelArguments.csproj | 1 + src/Samples/TopLevelArguments/WriteCommand.cs | 6 +- 24 files changed, 212 insertions(+), 149 deletions(-) create mode 100644 src/Samples/Subcommand/GeneratedManager.cs diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index f8905931..fea1ac34 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -204,7 +204,7 @@ private bool GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType } else { - builder.AppendArgument($"createParser: options => new CommandLineParser<{commandTypeName}>(options)"); + builder.AppendArgument($"createParser: options => new Ookii.CommandLine.CommandLineParser<{commandTypeName}>(options)"); } } 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 8a4bfc3e..8ea18ff1 100644 --- a/src/Samples/NestedCommands/CourseCommands.cs +++ b/src/Samples/NestedCommands/CourseCommands.cs @@ -15,21 +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.")] -[GeneratedParser] 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) { @@ -43,16 +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.")] -[GeneratedParser] 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/GeneratedManager.cs b/src/Samples/NestedCommands/GeneratedManager.cs index 9cb3512f..c1a32526 100644 --- a/src/Samples/NestedCommands/GeneratedManager.cs +++ b/src/Samples/NestedCommands/GeneratedManager.cs @@ -2,6 +2,7 @@ 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 36dd2ed2..6052a362 100644 --- a/src/Samples/NestedCommands/ListCommand.cs +++ b/src/Samples/NestedCommands/ListCommand.cs @@ -6,9 +6,9 @@ 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.")] -[GeneratedParser] 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 f1ba8224..7e51c7b7 100644 --- a/src/Samples/NestedCommands/NestedCommands.csproj +++ b/src/Samples/NestedCommands/NestedCommands.csproj @@ -16,8 +16,7 @@ This is sample code, so you can use it freely. --> - + + diff --git a/src/Samples/NestedCommands/Program.cs b/src/Samples/NestedCommands/Program.cs index bd1cb4cc..8e307e50 100644 --- a/src/Samples/NestedCommands/Program.cs +++ b/src/Samples/NestedCommands/Program.cs @@ -8,8 +8,10 @@ { // 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. + // 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, }, }; diff --git a/src/Samples/NestedCommands/README.md b/src/Samples/NestedCommands/README.md index 561b6012..f8ae29b4 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 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). 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)). +- Remove 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: @@ -112,9 +106,9 @@ Usage: NestedCommands student add [-FirstName] [-LastName] [[- We can see the usage syntax correctly 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 f2d918ab..b407dc35 100644 --- a/src/Samples/NestedCommands/StudentCommands.cs +++ b/src/Samples/NestedCommands/StudentCommands.cs @@ -15,23 +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.")] -[GeneratedParser] 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; } @@ -47,16 +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.")] -[GeneratedParser] 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) { @@ -74,24 +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.")] -[GeneratedParser] 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/Subcommand/EncodingConverter.cs b/src/Samples/Subcommand/EncodingConverter.cs index 822eca4c..340446a6 100644 --- a/src/Samples/Subcommand/EncodingConverter.cs +++ b/src/Samples/Subcommand/EncodingConverter.cs @@ -1,13 +1,11 @@ using Ookii.CommandLine; using Ookii.CommandLine.Conversion; -using System; using System.Globalization; using System.Text; namespace SubcommandSample; -// A ArgumentConverter for the Encoding class, using the utility base class provided by -// Ookii.CommandLine. +// An ArgumentConverter for the Encoding class. internal class EncodingConverter : ArgumentConverter { public override object? Convert(string value, CultureInfo culture, CommandLineArgument argument) 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..55877da2 100644 --- a/src/Samples/Subcommand/README.md +++ b/src/Samples/Subcommand/README.md @@ -4,11 +4,18 @@ 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. A publish profile for Visual Studio that trims the application +is included so you can try this out, or you can run `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. + When invoked without arguments, a subcommand application prints the list of commands. ```text @@ -32,7 +39,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 +79,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 c148d5c8..c7f3e864 100644 --- a/src/Samples/Subcommand/ReadCommand.cs +++ b/src/Samples/Subcommand/ReadCommand.cs @@ -1,11 +1,8 @@ using Ookii.CommandLine; using Ookii.CommandLine.Commands; using Ookii.CommandLine.Conversion; -using System; using System.ComponentModel; -using System.IO; using System.Text; -using System.Threading.Tasks; namespace SubcommandSample; @@ -20,19 +17,22 @@ 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.")] -class ReadCommand : AsyncCommandBase +partial class ReadCommand : AsyncCommandBase { // A required, positional argument to specify the file name. - [CommandLineArgument(Position = 0)] + [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 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.")] [ArgumentConverter(typeof(EncodingConverter))] public Encoding Encoding { get; set; } = Encoding.UTF8; @@ -42,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 9904827a..0f4c9a62 100644 --- a/src/Samples/Subcommand/Subcommand.csproj +++ b/src/Samples/Subcommand/Subcommand.csproj @@ -3,15 +3,23 @@ Exe net7.0 - disable + 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 de6c5cd1..714433b0 100644 --- a/src/Samples/Subcommand/WriteCommand.cs +++ b/src/Samples/Subcommand/WriteCommand.cs @@ -2,12 +2,8 @@ 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; @@ -22,34 +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.")] -class WriteCommand : AsyncCommandBase +partial class WriteCommand : AsyncCommandBase { // A required, positional argument to specify the file name. - [CommandLineArgument(Position = 0)] + [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 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.")] [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] diff --git a/src/Samples/TopLevelArguments/CommandUsageWriter.cs b/src/Samples/TopLevelArguments/CommandUsageWriter.cs index 61095f58..5d95579c 100644 --- a/src/Samples/TopLevelArguments/CommandUsageWriter.cs +++ b/src/Samples/TopLevelArguments/CommandUsageWriter.cs @@ -10,11 +10,6 @@ class CommandUsageWriter : UsageWriter // for that. public bool IncludeCommandUsageSyntax { get; set; } - public CommandUsageWriter() - { - IncludeCommandHelpInstruction = true; - } - // Indicate there are global arguments in the command usage syntax. protected override void WriteUsageSyntaxPrefix() { diff --git a/src/Samples/TopLevelArguments/EncodingConverter.cs b/src/Samples/TopLevelArguments/EncodingConverter.cs index 862d3655..4a80526e 100644 --- a/src/Samples/TopLevelArguments/EncodingConverter.cs +++ b/src/Samples/TopLevelArguments/EncodingConverter.cs @@ -1,12 +1,11 @@ using Ookii.CommandLine; using Ookii.CommandLine.Conversion; -using System; using System.Globalization; using System.Text; namespace TopLevelArguments; -// A ArgumentConverter for the Encoding class, using the utility base class provided by +// An ArgumentConverter for the Encoding class, using the utility base class provided by // Ookii.CommandLine. internal class EncodingConverter : ArgumentConverter { diff --git a/src/Samples/TopLevelArguments/Program.cs b/src/Samples/TopLevelArguments/Program.cs index 11151f32..2445ae70 100644 --- a/src/Samples/TopLevelArguments/Program.cs +++ b/src/Samples/TopLevelArguments/Program.cs @@ -17,31 +17,19 @@ static async Task Main() // output. CommandOptions inherits from ParseOptions so it supports all the same options. var commandOptions = new CommandOptions() { - // Use POSIX rules - Mode = ParsingMode.LongShort, - ArgumentNameComparison = StringComparison.InvariantCulture, - ArgumentNameTransform = NameTransform.DashCase, - ValueDescriptionTransform = NameTransform.DashCase, - CommandNameComparison = StringComparison.InvariantCulture, - CommandNameTransform = NameTransform.DashCase, + IsPosix = true, // The top-level arguments will have a -Version argument, so no need for a version // command. AutoVersionCommand = false, UsageWriter = commandUsageWriter, }; - // Create a CommandManager for the commands in the current assembly. - // - // In addition to our commands, it will also have an automatic "version" command (this can - // be disabled with the options). var manager = new GeneratedManager(commandOptions); + + // Use different options for the top-level arguments. var parseOptions = new ParseOptions() { - // Use POSIX rules - Mode = ParsingMode.LongShort, - ArgumentNameComparison = StringComparison.InvariantCultureIgnoreCase, - ArgumentNameTransform = NameTransform.DashCase, - ValueDescriptionTransform = NameTransform.DashCase, + IsPosix = true, // Modified usage format to list commands as well as top-level usage. UsageWriter = new TopLevelUsageWriter(manager) }; diff --git a/src/Samples/TopLevelArguments/README.md b/src/Samples/TopLevelArguments/README.md index af6eaaa4..986f6208 100644 --- a/src/Samples/TopLevelArguments/README.md +++ b/src/Samples/TopLevelArguments/README.md @@ -1,3 +1,86 @@ # Subcommands with top-level arguments sample -TODO \ No newline at end of file +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 help 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 index 6e2e07f6..3821cc7b 100644 --- a/src/Samples/TopLevelArguments/ReadCommand.cs +++ b/src/Samples/TopLevelArguments/ReadCommand.cs @@ -16,22 +16,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(Program.Arguments!.Path.FullName, Program.Arguments.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(Program.Arguments!.Path.FullName, Program.Arguments.Encoding)) { await writer.WriteLineAsync(line); } diff --git a/src/Samples/TopLevelArguments/TopLevelArguments.cs b/src/Samples/TopLevelArguments/TopLevelArguments.cs index a3558dd2..790dbaa4 100644 --- a/src/Samples/TopLevelArguments/TopLevelArguments.cs +++ b/src/Samples/TopLevelArguments/TopLevelArguments.cs @@ -10,7 +10,7 @@ namespace TopLevelArguments; partial class TopLevelArguments { // A required, positional argument to specify the file name. - [CommandLineArgument(Position = 0)] + [CommandLineArgument(IsPositional = true)] [Description("The path of the file to read or write.")] public required FileInfo Path { get; set; } @@ -18,14 +18,16 @@ partial class TopLevelArguments // // 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(Position = 1, CancelParsing = CancelMode.Success)] + [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. - [CommandLineArgument(IsShort = true)] + // 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 index a92af214..f93e0e0a 100644 --- a/src/Samples/TopLevelArguments/TopLevelArguments.csproj +++ b/src/Samples/TopLevelArguments/TopLevelArguments.csproj @@ -21,4 +21,5 @@ This is sample code, so you can use it freely. OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> + diff --git a/src/Samples/TopLevelArguments/WriteCommand.cs b/src/Samples/TopLevelArguments/WriteCommand.cs index 40fcc442..642c540c 100644 --- a/src/Samples/TopLevelArguments/WriteCommand.cs +++ b/src/Samples/TopLevelArguments/WriteCommand.cs @@ -13,15 +13,15 @@ namespace TopLevelArguments; partial class WriteCommand : AsyncCommandBase { // Positional multi-value argument to specify the text to write - [CommandLineArgument(Position = 0)] + [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(DefaultValue = 79, IsShort = true)] + [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; } + public int MaximumLineLength { get; set; } = 79; // A switch argument that indicates it's okay to overwrite files. [CommandLineArgument(IsShort = true)] From 9a704c1611fdf5169cb40b9a5ca58c2ff57bcbcf Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 17:20:18 -0700 Subject: [PATCH 175/234] Remove references to constructor parameters. --- docs/ChangeLog.md | 4 +- docs/UsageHelp.md | 4 +- src/Ookii.CommandLine/CommandLineArgument.cs | 47 +++++++++---------- src/Ookii.CommandLine/CommandLineParser.cs | 10 ++-- .../Commands/CommandManager.cs | 6 +-- src/Ookii.CommandLine/Commands/ICommand.cs | 5 +- 6 files changed, 35 insertions(+), 41 deletions(-) diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index 285db7d4..a301b5bd 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -110,8 +110,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. diff --git a/docs/UsageHelp.md b/docs/UsageHelp.md index d2c60a91..e2a9fc63 100644 --- a/docs/UsageHelp.md +++ b/docs/UsageHelp.md @@ -203,8 +203,8 @@ After the usage syntax, the usage help ends with a list of all arguments with th descriptions. The description of an argument can be specified using the [`DescriptionAttribute`][] attribute. -Apply this attribute to the constructor parameter or property defining the argument. It's strongly -recommended to add a description to every argument. +Apply this attribute to the property or method defining the argument. It's strongly recommended to +add a description to every argument. ```csharp [CommandLineArgument] diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 533eb1bd..66575326 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -412,10 +412,10 @@ internal CommandLineArgument(ArgumentInfo info) public CommandLineParser Parser => _parser; /// - /// Gets the name of the property, method, or constructor parameter that defined this command line argument. + /// Gets the name of the property or method that defined this command line argument. /// /// - /// The name of the property, method, or constructor parameter that defined this command line argument. + /// The name of the property or method that defined this command line argument. /// public string MemberName { @@ -642,12 +642,13 @@ public Type ArgumentType /// /// /// - /// A positional argument is created either using a constructor parameter on the command line arguments type, - /// or by using the property. + /// A positional argument is created by using the + /// or 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. + /// 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; } @@ -656,13 +657,14 @@ public Type ArgumentType /// 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. + /// 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 . + /// 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 @@ -693,12 +695,12 @@ public bool IsRequired /// /// /// - /// 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. + /// 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 is . + /// This value is only used if the property is . /// /// public object? DefaultValue @@ -739,7 +741,7 @@ public object? DefaultValue /// /// /// To set the description of an argument, apply the - /// attribute to the constructor parameter, property, or method that defines the argument. + /// attribute to the property or method that defines the argument. /// /// public string Description @@ -1029,7 +1031,7 @@ public bool AllowsDuplicateDictionaryKeys /// 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. + /// determine nullability of a property or method parameter. /// /// public bool AllowNull => _allowNull; @@ -1098,10 +1100,10 @@ public bool AllowsDuplicateDictionaryKeys /// 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 , or have a - /// static Parse(, ) or Parse() method, or have a constructor that takes a single parameter of type - /// . + /// 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 . /// /// /// @@ -1464,11 +1466,6 @@ internal static (CommandLineArgument, bool) CreateAutomaticHelp(CommandLineParse return new VersionArgument(parser, argumentName); } - internal object? GetConstructorParameterValue() - { - return Value; - } - internal void ApplyPropertyValue(object target) { // Do nothing for method-based values, or for required properties if the provider is not diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index e415b4f9..222bde13 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -778,11 +778,13 @@ public static string GetExecutableName(bool includeExtension = false) /// /// /// - /// The usage help consists of first the , followed by the usage syntax, followed by a description of all the arguments. + /// 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. + /// 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 @@ -1554,7 +1556,7 @@ private void VerifyPositionalArgumentRules() foreach (CommandLineArgument argument in _arguments) { - // Apply property argument values (this does nothing for constructor or method arguments). + // Apply property argument values (this does nothing for method arguments). argument.ApplyPropertyValue(commandLineArguments); } diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index 0375c511..720cadd9 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -31,10 +31,8 @@ namespace Ookii.CommandLine.Commands; /// the method to implement the command's functionality. /// /// -/// 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. +/// Subcommands classes are instantiated using the class, and +/// follow the same rules as command line arguments classes. /// /// /// Commands can be defined in a single assembly, or multiple assemblies. diff --git a/src/Ookii.CommandLine/Commands/ICommand.cs b/src/Ookii.CommandLine/Commands/ICommand.cs index a8ddb179..fefc467f 100644 --- a/src/Ookii.CommandLine/Commands/ICommand.cs +++ b/src/Ookii.CommandLine/Commands/ICommand.cs @@ -9,10 +9,7 @@ /// 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. -/// -/// +/// The class will be used as an arguments type with the . /// Alternatively, a command can implement its own argument parsing by implementing the /// interface. /// From eab2cf14e5024106d1ddab3f4394c9393d570521 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 19 Jun 2023 17:38:52 -0700 Subject: [PATCH 176/234] Update resource strings and remove unused. --- .../Properties/Resources.Designer.cs | 89 +++---------------- .../Properties/Resources.resx | 47 +++------- 2 files changed, 26 insertions(+), 110 deletions(-) diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index f10df5e4..04834395 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 { @@ -267,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.. /// @@ -322,7 +313,7 @@ internal static string EmptyKeyValueSeparator { } /// - /// Looks up a localized string similar to You must specify at least one name/value separator.. + /// Looks up a localized string similar to You must specify at least one name-value separator.. /// internal static string EmptyNameValueSeparators { get { @@ -438,15 +429,6 @@ internal static string InvalidTypeConverter { } } - /// - /// Looks up a localized string similar to The IsSpanValid method must be overridden if CanValidateSpan is set to true.. - /// - internal static string IsSpanValidNotImplemented { - get { - return ResourceManager.GetString("IsSpanValidNotImplemented", resourceCulture); - } - } - /// /// Looks up a localized string similar to The 'minimum' and 'maximum' parameters cannot both be null.. /// @@ -493,16 +475,7 @@ 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.. - /// - internal static string MultipleMarkedConstructors { - get { - return ResourceManager.GetString("MultipleMarkedConstructors", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No argument converter exists for type '{0}'. Use the ArgumentConverterAttribute to specify a custom ArgumentConverter.. + /// 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 NoArgumentConverterFormat { get { @@ -510,15 +483,6 @@ internal static string NoArgumentConverterFormat { } } - /// - /// Looks up a localized string similar to The command line arguments type does not have any public constructors.. - /// - internal static string NoConstructor { - get { - return ResourceManager.GetString("NoConstructor", resourceCulture); - } - } - /// /// Looks up a localized string similar to The command does not use custom parsing.. /// @@ -529,7 +493,7 @@ internal static string NoCustomParsing { } /// - /// 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 { @@ -546,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.. /// @@ -583,7 +538,7 @@ internal static string NullPropertyValue { } /// - /// 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 { @@ -591,15 +546,6 @@ internal static string PropertyIsReadOnlyFormat { } } - /// - /// Looks up a localized string similar to The arguments type {0} has the GeneratedParserAttribute applied, but reflection is being used to create a parser for it. Use the generated {0}.CreateParser() or {0}.Parse() methods to use the generated parser. For subcommands, use the GeneratedCommandManagerAttribute.. - /// - internal static string ReflectionWithGeneratedParserFormat { - get { - return ResourceManager.GetString("ReflectionWithGeneratedParserFormat", resourceCulture); - } - } - /// /// Looks up a localized string similar to RequiresAnyAttribute requires at least two arguments; use CommandLineArgumentAttribute.IsRequired to make a single argument required.. /// @@ -627,15 +573,6 @@ internal static string TooManyArguments { } } - /// - /// Looks up a localized string similar to Could not convert type '{0}' to '{1}' for argument '{2}'.. - /// - internal static string TypeConversionErrorFormat { - get { - return ResourceManager.GetString("TypeConversionErrorFormat", resourceCulture); - } - } - /// /// Looks up a localized string similar to The type '{0}' does not implement the ICommand interface or does not have the CommandAttribute attribute.. /// @@ -709,7 +646,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 { @@ -718,7 +655,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 { @@ -736,7 +673,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 { @@ -745,7 +682,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 { @@ -925,7 +862,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 { @@ -934,7 +871,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 { @@ -952,7 +889,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 { @@ -961,7 +898,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 465420f2..05a22d21 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. @@ -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 argument converter exists for type '{0}'. Use the ArgumentConverterAttribute to specify a custom ArgumentConverter. + 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}'. @@ -399,9 +384,6 @@ The command does not use custom parsing. - - The arguments type {0} has the GeneratedParserAttribute applied, but reflection is being used to create a parser for it. Use the generated {0}.CreateParser() or {0}.Parse() methods to use the generated parser. For subcommands, use the GeneratedCommandManagerAttribute. - Invalid value for the StringComparison enumeration. @@ -409,7 +391,7 @@ The specified TypeConverter cannot converter from a string. - You must specify at least one name/value separator. + 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. @@ -420,9 +402,6 @@ Cannot get or set the property for this argument. - - The IsSpanValid method must be overridden if CanValidateSpan is set to true. - A read-only property for a multi-value or dictionary argument returned null. From 2d2766e95b1fdd51e9b925860287a8c042bae645 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 20 Jun 2023 18:36:50 -0700 Subject: [PATCH 177/234] XML comment updates. --- src/Ookii.CommandLine/CommandLineParser.cs | 341 +++++++++++---------- 1 file changed, 184 insertions(+), 157 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 222bde13..ca15e2fa 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -15,27 +15,26 @@ namespace Ookii.CommandLine; /// -/// Parses command line arguments defined by a class of the specified type. +/// Parses command line arguments defined by a type's properties and methods. /// /// /// -/// The class can parse a set of command line arguments into -/// values. Which arguments are accepted is determined from 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 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 parameter, or a single parameter -/// with the type , which will be passed the instance of the -/// class that was used to parse the arguments when the type -/// is instantiated. +/// 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 -/// defined. The properties of the argument are determined by the properties of the -/// class. +/// 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 , , @@ -44,23 +43,21 @@ namespace Ookii.CommandLine; /// attribute. /// /// -/// 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. +/// 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 derived type also provides strongly-typed instance -/// methods, if you don't wish to use the static +/// 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 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. +/// 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. @@ -69,15 +66,15 @@ namespace Ookii.CommandLine; /// /// /// The supports two sets of rules for how to parse arguments; -/// mode and mode. For +/// mode and mode. For /// more details on these rules, please see -/// the documentation on GitHub. +/// the documentation on GitHub. /// /// /// /// /// -/// Usage documentation +/// Usage documentation public class CommandLineParser { #region Nested types @@ -204,7 +201,7 @@ private struct PrefixInfo /// /// Gets the default prefix used for long argument names if is - /// . + /// . /// /// /// The default long argument name prefix, which is '--'. @@ -222,18 +219,18 @@ private struct PrefixInfo ///
/// /// - /// If the event handler sets the property to , command line processing will stop immediately, + /// 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 + /// 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. + /// This event is invoked after the and properties have been set. /// /// public event EventHandler? ArgumentParsed; @@ -277,8 +274,9 @@ private struct PrefixInfo /// /// This constructor uses reflection to determine the arguments defined by the type indicated /// by at runtime, unless the type has the - /// applied. In that case, you can also use the - /// generated static CreateParser() or Parse() methods on that type instead. + /// 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 @@ -316,11 +314,14 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) /// /// 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. + /// 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. @@ -385,11 +386,11 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// Gets the command line argument parsing rules used by the parser. ///
/// - /// The for this parser. The default is - /// . + /// The for this parser. The default is + /// . /// - /// - /// + /// + /// public ParsingMode Mode => _mode; /// @@ -400,34 +401,36 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - /// The argument name prefixes are used to distinguish argument names from positional argument values in a command line. + /// 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 + /// 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 - /// is not . + /// The prefix for long argument names, or if the + /// property is not . /// /// /// - /// The long argument prefix is only used if property is - /// . See to - /// get the prefixes for short argument names. + /// 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; /// @@ -439,7 +442,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null public Type ArgumentsType => _provider.ArgumentsType; /// - /// Gets the friendly name of the application. + /// Gets the friendly name of the application for use in the version information. /// /// /// The friendly name of the application. @@ -466,9 +469,9 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - /// This description will be added to the usage returned by the - /// method. This description can be set by applying the - /// to the command line arguments type. + /// If not empty, this description will be added 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; @@ -481,11 +484,11 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - /// If you change the value of the , , - /// , or + /// 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 + /// class is constructed, so changing them afterwards will /// have no effect. /// /// @@ -496,14 +499,14 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// The culture used to convert command line argument values from their string representation to the argument type. The default value - /// is . + /// is . /// /// /// /// Use the class to change this value. /// /// - /// + /// public CultureInfo Culture => _parseOptions.CultureOrDefault; /// @@ -526,41 +529,45 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// dictionary arguments, which can always be supplied multiple times. /// /// - /// Use the or class to + /// Use the or class to /// change this value. /// /// - /// - /// + /// + /// public bool AllowDuplicateArguments => _parseOptions.DuplicateArgumentsOrDefault != ErrorMode.Error; /// - /// Gets value indicating whether the value of an argument may be in a separate - /// argument from its name. + /// 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 arguments; if the characters - /// specified in the property must be used. The default - /// value is . + /// 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. + /// 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. + /// 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. + /// 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 ), + /// 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 @@ -569,12 +576,12 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// property is . /// /// - /// Use the or class to + /// Use the or class to /// change this value. /// /// - /// - /// + /// + /// public bool AllowWhiteSpaceValueSeparator => _parseOptions.AllowWhiteSpaceValueSeparatorOrDefault; /// @@ -585,12 +592,13 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - /// Use the or class to + /// Use the or class to /// change this value. /// /// - /// - /// + /// + /// + /// public ImmutableArray NameValueSeparators => _nameValueSeparators; /// @@ -602,33 +610,34 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - /// Check this property after calling the method - /// to see if usage help should be displayed. + /// 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 be if the - /// method threw a , if an argument used - /// , if parsing was canceled - /// using the event. + /// 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 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. + /// 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 bool MethodArgument(CommandLineParser parser) + /// public static CancelMode MethodArgument(CommandLineParser parser) /// { /// parser.HelpRequested = true; - /// return false; + /// return CancelMode.Abort; /// } /// - /// - /// The property will always be if - /// did not throw and returned a non-null value. - /// /// public bool HelpRequested { get; set; } @@ -639,7 +648,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// An instance of a class inheriting from the class. /// - /// + /// public LocalizedStringProvider StringProvider => _parseOptions.StringProvider; /// @@ -652,13 +661,13 @@ public IEnumerable Validators => ArgumentsType.GetCustomAttributes(); /// - /// Gets the string comparer used for argument names. + /// Gets the string comparison used for argument names. /// /// - /// One of the members of the enumeration. + /// One of the values of the enumeration. /// - /// - /// + /// + /// public StringComparison ArgumentNameComparison { get; } /// @@ -672,23 +681,39 @@ public IEnumerable Validators /// 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. + /// 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. + /// 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 call to the method. + /// Gets the result of the last command line argument parsing operation. /// /// - /// An instance of the class. + /// An instance of the class. /// /// /// @@ -703,7 +728,7 @@ public IEnumerable Validators /// Gets the kind of provider that was used to determine the available arguments. /// /// - /// One of the values of the enumeration. + /// One of the values of the enumeration. /// public ProviderKind ProviderKind => _provider.Kind; @@ -722,19 +747,19 @@ public IEnumerable Validators /// /// /// - /// To determine the executable name, this method first checks the + /// 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 + /// 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 + /// the usage syntax when generating usage help, unless overridden by the /// property. /// /// - /// + /// public static string GetExecutableName(bool includeExtension = false) { string? path = null; @@ -773,7 +798,7 @@ public static string GetExecutableName(bool includeExtension = false) /// /// /// The to use to create the usage. If , - /// the value from the property in the + /// the value from the property in the /// property is sued. /// /// @@ -807,8 +832,8 @@ public void WriteUsage(UsageWriter? usageWriter = null) /// /// /// The to use to create the usage. If , - /// the value from the property in the - /// property is sued. + /// the value from the property in the + /// property is used. /// /// /// A string containing usage help for the command line options defined by the type @@ -824,14 +849,15 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = } /// - /// Parses the arguments returned by the + /// 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 . + /// event handler, the property, + /// or a method argument that returned or + /// . /// /// /// @@ -840,7 +866,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// - /// An error occurred parsing the command line. Check the + /// An error occurred parsing the command line. Check the /// property for the exact reason for the error. /// public object? Parse() @@ -898,13 +924,13 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = } /// - /// Parses the arguments returned by the + /// 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 + /// property or a method argument /// that returned . /// /// @@ -1005,7 +1031,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = } /// - /// Parses the arguments returned by the + /// Parses the arguments returned by the /// method using the type . /// /// The type defining the command line arguments. @@ -1015,8 +1041,9 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// An instance of the type , or if an - /// error occurred, or argument parsing was canceled by the - /// property or a method argument that returned . + /// error occurred, or argument parsing was canceled by the + /// property or a method argument that returned + /// or . /// /// /// @@ -1030,32 +1057,34 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// 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 + /// 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 + /// 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 . + /// 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 + /// 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. + /// 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. In - /// that case, you can also use the generated static CreateParser() or Parse() - /// methods on that type instead. + /// 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 @@ -1088,15 +1117,9 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// does not fall within the bounds of . /// - /// - /// - /// /// /// /// - /// - /// - /// /// /// /// @@ -1132,9 +1155,6 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// - /// - /// - /// /// /// /// @@ -1155,8 +1175,15 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// the argument, or if the argument was not found. /// is . /// - /// If the property is , this uses + /// + /// 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) { @@ -1176,14 +1203,14 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = } /// - /// Gets a command line argument by short name. + /// 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 + /// If is not , this /// method always returns /// /// @@ -1224,14 +1251,14 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// - /// If the property is , these + /// 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) @@ -1240,7 +1267,7 @@ public static ImmutableArray GetDefaultArgumentNamePrefixes() } /// - /// Gets the default character used to separate the name and the value of an argument. + /// 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 From 64544765a71500a4a72d1e4748d5446b15c25ff8 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 21 Jun 2023 11:30:00 -0700 Subject: [PATCH 178/234] Add qualifyHint to seealso and see. --- src/Ookii.CommandLine/AliasAttribute.cs | 2 +- .../AllowDuplicateDictionaryKeysAttribute.cs | 6 +- .../ArgumentParsedEventArgs.cs | 14 +- src/Ookii.CommandLine/CancelMode.cs | 10 +- src/Ookii.CommandLine/CommandLineArgument.cs | 102 +++++----- .../CommandLineArgumentAttribute.cs | 86 ++++----- .../CommandLineArgumentErrorCategory.cs | 6 +- .../CommandLineArgumentException.cs | 4 +- .../CommandLineParserGeneric.cs | 4 +- .../Commands/AsyncCommandBase.cs | 4 +- .../Commands/CommandAttribute.cs | 8 +- src/Ookii.CommandLine/Commands/CommandInfo.cs | 16 +- .../Commands/CommandManager.cs | 146 +++++++-------- .../Commands/CommandOptions.cs | 36 ++-- .../Commands/IAsyncCommand.cs | 6 +- .../Commands/ParentCommand.cs | 6 +- .../Commands/ParentCommandAttribute.cs | 2 +- .../Conversion/KeyValuePairConverter.cs | 2 +- .../Conversion/ParsableConverter.cs | 2 +- .../Conversion/SpanParsableConverter.cs | 2 +- .../DescriptionListFilterMode.cs | 2 +- .../DescriptionListSortMode.cs | 6 +- .../DuplicateArgumentEventArgs.cs | 2 +- src/Ookii.CommandLine/ErrorMode.cs | 2 +- src/Ookii.CommandLine/IParser.cs | 14 +- src/Ookii.CommandLine/IParserProvider.cs | 2 +- .../LineWrappingTextWriter.cs | 14 +- .../LocalizedStringProvider.Error.cs | 28 +-- .../LocalizedStringProvider.Validators.cs | 4 +- .../LocalizedStringProvider.cs | 22 +-- .../MultiValueSeparatorAttribute.cs | 2 +- src/Ookii.CommandLine/NameTransform.cs | 8 +- .../NameTransformExtensions.cs | 2 +- src/Ookii.CommandLine/ParseOptions.cs | 174 +++++++++--------- .../ParseOptionsAttribute.cs | 116 ++++++------ src/Ookii.CommandLine/ParseResult.cs | 18 +- src/Ookii.CommandLine/ParseStatus.cs | 4 +- src/Ookii.CommandLine/ParsingMode.cs | 10 +- src/Ookii.CommandLine/ShortAliasAttribute.cs | 12 +- .../Support/ArgumentProvider.cs | 2 +- .../Support/GeneratedArgument.cs | 2 +- .../Terminal/VirtualTerminal.cs | 6 +- src/Ookii.CommandLine/UsageHelpRequest.cs | 6 +- src/Ookii.CommandLine/UsageWriter.cs | 102 +++++----- .../Validation/ArgumentValidationAttribute.cs | 40 ++-- .../ArgumentValidationWithHelpAttribute.cs | 6 +- .../Validation/ClassValidationAttribute.cs | 10 +- .../DependencyValidationAttribute.cs | 4 +- .../Validation/ProhibitsAttribute.cs | 2 +- .../Validation/RequiresAnyAttribute.cs | 6 +- .../Validation/RequiresAttribute.cs | 2 +- .../Validation/ValidateCountAttribute.cs | 4 +- .../Validation/ValidateEnumValueAttribute.cs | 4 +- .../Validation/ValidateNotEmptyAttribute.cs | 2 +- .../Validation/ValidateNotNullAttribute.cs | 2 +- .../ValidateNotWhiteSpaceAttribute.cs | 2 +- .../Validation/ValidatePatternAttribute.cs | 4 +- .../Validation/ValidateRangeAttribute.cs | 2 +- .../ValidateStringLengthAttribute.cs | 2 +- .../Validation/ValidationMode.cs | 8 +- .../ValueDescriptionAttribute.cs | 12 +- src/Ookii.CommandLine/WrappingMode.cs | 2 +- 62 files changed, 569 insertions(+), 569 deletions(-) diff --git a/src/Ookii.CommandLine/AliasAttribute.cs b/src/Ookii.CommandLine/AliasAttribute.cs index 59d1129f..ee15bd04 100644 --- a/src/Ookii.CommandLine/AliasAttribute.cs +++ b/src/Ookii.CommandLine/AliasAttribute.cs @@ -34,7 +34,7 @@ namespace Ookii.CommandLine; /// /// /// -/// +/// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = true)] public sealed class AliasAttribute : Attribute { diff --git a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs index cd5738d8..c890a15f 100644 --- a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs +++ b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs @@ -12,15 +12,15 @@ namespace Ookii.CommandLine; /// , 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. +/// 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/ArgumentParsedEventArgs.cs b/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs index 5440f44c..72af4ace 100644 --- a/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs +++ b/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs @@ -3,7 +3,7 @@ namespace Ookii.CommandLine; /// -/// Provides data for the event. +/// Provides data for the event. /// /// public class ArgumentParsedEventArgs : EventArgs @@ -37,22 +37,22 @@ public CommandLineArgument Argument /// /// /// One of the values of the enumeration. The default value is the - /// value of the attribute, or the + /// value of the attribute, or the /// return value of a method argument. /// /// /// - /// If the event handler sets this property to a value other than , + /// 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 + /// If you want usage help to be displayed after canceling, set the /// property to . /// /// - /// - /// - /// + /// + /// + /// public CancelMode CancelParsing { get; set; } } diff --git a/src/Ookii.CommandLine/CancelMode.cs b/src/Ookii.CommandLine/CancelMode.cs index 93bd8ab7..7916a53b 100644 --- a/src/Ookii.CommandLine/CancelMode.cs +++ b/src/Ookii.CommandLine/CancelMode.cs @@ -3,7 +3,7 @@ /// /// Indicates whether and how the argument should cancel parsing. /// -/// +/// /// public enum CancelMode { @@ -13,14 +13,14 @@ public enum CancelMode None, /// /// The argument cancels parsing, discarding the results so far. Parsing, using for example the - /// method, will return a - /// value. The property will be . + /// 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 . + /// 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 66575326..578f0333 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -431,15 +431,15 @@ public string MemberName /// /// /// 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 . + /// generated by . /// /// - /// If the property is , + /// 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; /// @@ -450,10 +450,10 @@ public string MemberName /// /// /// - /// The short name is only used if the parser is using . + /// The short name is only used if the parser is using . /// /// - /// + /// public char ShortName => _shortName; /// @@ -464,13 +464,13 @@ public string MemberName /// /// /// - /// If the property is , + /// 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. + /// For , the prefix used is the first prefix specified + /// in the property. /// /// public string ArgumentNameWithPrefix @@ -489,8 +489,8 @@ public string ArgumentNameWithPrefix /// Gets the long argument name with the long prefix. /// /// - /// The long argument name with its prefix, or if the - /// property is not or the + /// The long argument name with its prefix, or if the + /// property is not or the /// property is . /// public string? LongNameWithPrefix @@ -507,13 +507,13 @@ public string? LongNameWithPrefix /// 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 + /// 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 + /// The prefix used is the first prefix specified in the /// property. /// /// @@ -535,11 +535,11 @@ public string? ShortNameWithPrefix /// /// /// - /// The short name is only used if the parser is using . + /// The short name is only used if the parser is using . /// Otherwise, this property is always . /// /// - /// + /// public bool HasShortName => _shortName != '\0'; /// @@ -550,11 +550,11 @@ public string? ShortNameWithPrefix /// /// /// - /// If the property is not , + /// If the property is not , /// this property is always . /// /// - /// + /// public bool HasLongName => _hasLongName; /// @@ -565,7 +565,7 @@ public string? ShortNameWithPrefix /// /// /// - /// If the property is , + /// If the property is , /// and the property is , this property /// will always return an empty collection. /// @@ -581,7 +581,7 @@ public string? ShortNameWithPrefix /// /// /// - /// If the property is not , + /// If the property is not , /// or the property is , this property /// will always return an empty collection. /// @@ -642,12 +642,12 @@ public Type ArgumentType /// /// /// - /// A positional argument is created by using the - /// or property. + /// 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 + /// This doesn't need to match the original value of the /// property. /// /// @@ -662,7 +662,7 @@ public Type ArgumentType /// /// /// - /// An argument is required if its , + /// 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. /// @@ -695,7 +695,7 @@ public bool IsRequired /// /// /// - /// The default value is set by the + /// The default value is set by the /// property, or when the is used it can also be /// specified using a property initializer. /// @@ -718,12 +718,12 @@ public object? DefaultValue /// /// /// - /// This value is set by the + /// 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 + /// , and if both this property and the /// property are . /// /// @@ -737,7 +737,7 @@ public object? DefaultValue /// /// /// - /// This property is used only when generating usage information using . + /// This property is used only when generating usage information using . /// /// /// To set the description of an argument, apply the @@ -759,8 +759,8 @@ public string Description /// /// 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 + /// 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. /// @@ -774,7 +774,7 @@ public string Description /// /// /// - /// + /// public string ValueDescription => _valueDescription ??= DetermineValueDescription(); /// @@ -803,13 +803,13 @@ public string Description /// /// /// - /// An argument that is can accept multiple values + /// 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 + /// 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 @@ -817,12 +817,12 @@ public string Description /// property. /// /// - /// An argument is if it is backed by a method instead + /// 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 . + /// Otherwise, the value will be . /// /// /// @@ -832,8 +832,8 @@ public string Description /// Gets a value indicating whether this argument is a multi-value argument. /// /// - /// if the property is - /// or ; otherwise, . + /// if the property is + /// or ; otherwise, . /// /// /// @@ -883,12 +883,12 @@ public string? MultiValueSeparator /// Gets the separator for key/value pairs if this argument is a dictionary argument. /// /// - /// The custom value specified using the attribute, or + /// 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 . + /// This property is only meaningful if the property is . /// /// /// @@ -898,7 +898,7 @@ public string? MultiValueSeparator /// Gets a value indicating whether this argument is a dictionary argument. /// /// - /// if this the property is ; + /// if this the property is ; /// otherwise, . /// public bool IsDictionary => _argumentKind == ArgumentKind.Dictionary; @@ -911,7 +911,7 @@ public string? MultiValueSeparator /// /// /// - /// This property is only meaningful if the property is . + /// This property is only meaningful if the property is . /// /// /// @@ -921,7 +921,7 @@ public bool AllowsDuplicateDictionaryKeys } /// - /// Gets the value that the argument was set to in the last call to . + /// 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. @@ -929,7 +929,7 @@ public bool AllowsDuplicateDictionaryKeys /// /// /// The property provides an alternative method for accessing supplied argument - /// values, in addition to using the object returned by . + /// values, in addition to using the object returned by . /// /// /// If an argument was supplied on the command line, the property will equal the @@ -941,12 +941,12 @@ public bool AllowsDuplicateDictionaryKeys /// the property, and will be . /// /// - /// If the property is , the property will + /// 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 + /// If the property is , the property will /// return a with all the values, even if the argument type is a different type. /// /// @@ -954,7 +954,7 @@ public bool AllowsDuplicateDictionaryKeys /// /// Gets a value indicating whether the value of this argument was supplied on the command line in the last - /// call to . + /// call to . /// /// /// if this argument's value was supplied on the command line when the arguments were parsed; otherwise, . @@ -1017,13 +1017,13 @@ public bool AllowsDuplicateDictionaryKeys /// /// /// This property indicates what happens when the used for this argument returns - /// from its + /// 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 . + /// parsing with . /// /// /// If the project containing the command line argument type does not use nullable reference types, or does @@ -1045,7 +1045,7 @@ public bool AllowsDuplicateDictionaryKeys /// /// /// - /// This value is determined using the + /// This value is determined using the /// property. /// /// @@ -1061,7 +1061,7 @@ public bool AllowsDuplicateDictionaryKeys /// /// /// A hidden argument will not be included in the usage syntax or the argument description - /// list, even if is used. It does not + /// list, even if is used. It does not /// affect whether the argument can be used. /// /// @@ -1130,11 +1130,11 @@ public bool AllowsDuplicateDictionaryKeys /// 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 cannot be + /// method, using . Other types cannot be /// converted. /// /// - /// This method is used to convert the + /// 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. /// diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index b9dd46c8..723f1253 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -37,17 +37,17 @@ namespace Ookii.CommandLine; /// /// /// The return type must be either , or . -/// Using is equivalent to returning , and when +/// Using is equivalent to returning , and when /// using , returning is equivalent to returning -/// . +/// . /// /// /// Unlike using the property event, canceling parsing with the return /// value does not automatically print the usage help when using the -/// method, the -/// method or the +/// method, the +/// method or the /// class. Instead, it must be requested using by setting the -/// property to in the +/// property to in the /// target method. /// /// @@ -67,15 +67,15 @@ public sealed class CommandLineArgumentAttribute : Attribute /// /// The name of the argument, or to indicate the member name /// should be used, applying the specified by the - /// property or the + /// property or the /// property. /// /// - /// If the property is , + /// If the property is , /// the parameter is the long name of the argument. /// /// - /// If the property is + /// If the property is /// and the property is , the /// parameter will not be used. /// @@ -98,16 +98,16 @@ public CommandLineArgumentAttribute(string? argumentName = null) /// /// /// - /// If the property is , + /// If the property is , /// this is the long name of the argument. /// /// - /// If the property is + /// If the property is /// and the property is , the /// property is ignored. /// /// - /// + /// public string? ArgumentName { get { return _argumentName; } @@ -122,16 +122,16 @@ public string? ArgumentName /// /// /// - /// This property is ignored if is not - /// . + /// This property is ignored if is not + /// . /// /// - /// If the property is + /// If the property is /// and the property is , the /// property is ignored. /// /// - /// + /// public bool IsLong { get; set; } = true; /// @@ -143,15 +143,15 @@ public string? ArgumentName /// /// /// - /// This property is ignored if is not - /// . + /// 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'; @@ -164,8 +164,8 @@ public bool IsShort /// The short name, or a null character ('\0') if the argument has no short name. /// /// - /// This property is ignored if is not - /// . + /// This property is ignored if is not + /// . /// /// /// Setting this property implies the property is . @@ -176,7 +176,7 @@ public bool IsShort /// property. /// /// - /// + /// public char ShortName { get; set; } /// @@ -186,7 +186,7 @@ public bool IsShort /// if the argument must be supplied on the command line; otherwise, . /// The default value is . /// - /// + /// public bool IsRequired { get; set; } /// @@ -214,11 +214,11 @@ public bool IsShort /// necessary to set the property. /// /// - /// The property will be set to reflect the actual position of the 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; /// @@ -266,10 +266,10 @@ public bool IsPositional /// attribute was applied to a method. /// /// - /// By default, the command line usage help generated by + /// 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 + /// property to /// . /// /// @@ -280,7 +280,7 @@ public bool IsPositional /// property are shown in the usage help. /// /// - /// + /// public object? DefaultValue { get; set; } /// @@ -298,7 +298,7 @@ public bool IsPositional /// initializer. /// /// - /// This property is ignored if the + /// This property is ignored if the /// property is . /// /// @@ -313,38 +313,38 @@ public bool IsPositional /// /// /// - /// If this property is not , the + /// 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 , + /// 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 + /// 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 + /// 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 + /// Use the property to determine which argument caused /// cancellation. /// /// - /// The method and the - /// static helper method - /// will print usage information if parsing was canceled with . + /// The method and the + /// static helper method + /// will print usage information if parsing was canceled with . /// /// - /// Canceling parsing in this way is identical to handling the - /// event and setting property. + /// Canceling parsing in this way is identical to handling the + /// event and setting property. /// /// /// It's possible to prevent cancellation when an argument has this property set by - /// handling the event and setting the - /// property to . + /// handling the event and setting the + /// property to . /// /// - /// + /// public CancelMode CancelParsing { get; set; } /// @@ -357,13 +357,13 @@ public bool IsPositional /// /// /// A hidden argument will not be included in the usage syntax or the argument description - /// list, even if is used. + /// 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 beed6a69..026253b8 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs @@ -57,13 +57,13 @@ public enum CommandLineArgumentErrorCategory /// CombinedShortNameNonSwitch, /// - /// An instance of a class derived from the + /// 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. + /// 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 a5097560..64d36b60 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentException.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentException.cs @@ -8,12 +8,12 @@ namespace Ookii.CommandLine; /// /// /// -/// This exception indicates that the command line passed to the method +/// 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 +/// 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. /// /// diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index 3e04b5e8..52dd3881 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -19,7 +19,7 @@ namespace Ookii.CommandLine; /// /// 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. +/// should be used instead. /// /// public class CommandLineParser : CommandLineParser @@ -64,7 +64,7 @@ public CommandLineParser(ParseOptions? options = null) /// names or positions, or has an argument type that cannot be parsed. /// /// - /// The property for the + /// The property for the /// if a different type than . /// /// diff --git a/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs b/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs index 1ef17765..c0f4bd85 100644 --- a/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs +++ b/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs @@ -3,8 +3,8 @@ namespace Ookii.CommandLine.Commands; /// -/// Base class for asynchronous tasks that want the method to -/// invoke the method. +/// Base class for asynchronous tasks that want the method to +/// invoke the method. /// public abstract class AsyncCommandBase : IAsyncCommand { diff --git a/src/Ookii.CommandLine/Commands/CommandAttribute.cs b/src/Ookii.CommandLine/Commands/CommandAttribute.cs index 1e023601..8d7fd5c9 100644 --- a/src/Ookii.CommandLine/Commands/CommandAttribute.cs +++ b/src/Ookii.CommandLine/Commands/CommandAttribute.cs @@ -17,7 +17,7 @@ namespace Ookii.CommandLine.Commands; /// /// /// If a command has no explicit name, its name is determined by taking the type name -/// and applying the transformation specified by the +/// and applying the transformation specified by the /// property. /// /// @@ -38,7 +38,7 @@ public sealed class CommandAttribute : Attribute /// /// /// If a command has no explicit name, its name is determined by taking the type name - /// and applying the transformation specified by the + /// and applying the transformation specified by the /// property. /// /// @@ -49,7 +49,7 @@ 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. + /// The name of the command, which can be used to locate it using the method. /// /// is . /// @@ -59,7 +59,7 @@ public CommandAttribute(string commandName) } /// - /// Gets the name of the command, which can be used to locate it using the method. + /// 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 diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index bc73c6db..2e9153f2 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -68,9 +68,9 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// /// /// - /// The name is taken from the property. If + /// 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 + /// type's name, and applying the transformation specified by the /// property. /// /// @@ -115,7 +115,7 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// displayed, but can still be invoked from the command line. /// /// - /// + /// public bool IsHidden => _attribute.IsHidden; /// @@ -146,7 +146,7 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// /// /// The class will only use commands whose parent command - /// type matches the value of the property. + /// type matches the value of the property. /// /// public Type? ParentCommandType { get; } @@ -181,8 +181,8 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// /// /// - /// The property of the returned - /// will be if the command used custom parsing. + /// The property of the returned + /// will be if the command used custom parsing. /// /// /// @@ -214,8 +214,8 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// /// /// - /// The property of the returned - /// will be if the command used custom parsing. + /// The property of the returned + /// will be if the command used custom parsing. /// /// public (ICommand?, ParseResult) CreateInstanceWithResult(ReadOnlyMemory args) diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index 720cadd9..fc22c73f 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -28,7 +28,7 @@ namespace Ookii.CommandLine.Commands; /// /// 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 method to implement the command's functionality. /// /// /// Subcommands classes are instantiated using the class, and @@ -182,17 +182,17 @@ public CommandManager(IEnumerable assemblies, CommandOptions? 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 value of the property after the call to the + /// method made while creating /// the command. /// /// /// - /// If the was not invoked, for + /// 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 - /// . + /// custom parsing, the value of the property will be + /// . /// /// public ParseResult ParseResult { get; private set; } @@ -201,7 +201,7 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// Gets the kind of used to supply the commands. /// /// - /// One of the values of the enumeration. + /// One of the values of the enumeration. /// public ProviderKind ProviderKind => _provider.Kind; @@ -213,18 +213,18 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not returned. /// /// - /// If the is , only + /// If the 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 added if the + /// The automatic version command is added if the /// property is and there is no command with a conflicting name. /// /// @@ -263,32 +263,32 @@ public IEnumerable GetCommands() /// the same name, the first matching one will be returned. /// /// - /// If the property is , + /// 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 + /// 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 + /// 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 + /// Commands that don't meet the criteria of the /// predicate are not returned. /// /// - /// If the is , only + /// If the 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 + /// 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. /// @@ -357,29 +357,29 @@ public IEnumerable GetCommands() /// /// /// 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 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 + /// 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 . + /// be wrapped, depending on the value returned by . /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not returned. /// /// - /// If the is , only + /// If the 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 + /// 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. /// @@ -468,7 +468,7 @@ public IEnumerable GetCommands() } /// - /// Finds and instantiates the subcommand using the arguments from , + /// 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. /// /// @@ -492,7 +492,7 @@ public IEnumerable GetCommands() /// The arguments to the command. /// The index in at which to start parsing the arguments. /// - /// The value returned by , or if + /// The value returned by , or if /// the command could not be created. /// /// @@ -504,14 +504,14 @@ public IEnumerable GetCommands() /// /// /// This function creates the command by invoking the , - /// method and then invokes the method on the command. + /// method and then invokes the method on the command. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -531,20 +531,20 @@ public IEnumerable GetCommands() /// The name of the command. /// The arguments to the command. /// - /// The value returned by , or if + /// 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. + /// method and then invokes the method on the command. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -565,14 +565,14 @@ public IEnumerable GetCommands() /// /// /// This function creates the command by invoking the , - /// method and then invokes the method on the command. + /// method and then invokes the method on the command. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -593,14 +593,14 @@ public IEnumerable GetCommands() /// /// /// This function creates the command by invoking the , - /// method and then invokes the method on the command. + /// method and then invokes the method on the command. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -614,7 +614,7 @@ public IEnumerable GetCommands() } /// - /// Finds and instantiates the subcommand using the arguments from the + /// 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. /// @@ -624,14 +624,14 @@ public IEnumerable GetCommands() /// /// /// This function creates the command by invoking the , - /// method and then invokes the method on the command. + /// method and then invokes the method on the command. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -651,22 +651,22 @@ public IEnumerable GetCommands() /// /// /// A task representing the asynchronous run operation. The result is the value returned - /// by , or if the command + /// 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. + /// invokes the method; otherwise, it invokes the + /// method on the command. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -691,22 +691,22 @@ public IEnumerable GetCommands() /// /// /// A task representing the asynchronous run operation. The result is the value returned - /// by , or if the command + /// 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. + /// invokes the method; otherwise, it invokes the + /// method on the command. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -733,15 +733,15 @@ public IEnumerable GetCommands() /// /// 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. + /// invokes the method; otherwise, it invokes the + /// method on the command. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -768,15 +768,15 @@ public IEnumerable GetCommands() /// /// 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. + /// invokes the method; otherwise, it invokes the + /// method on the command. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -796,7 +796,7 @@ public IEnumerable GetCommands() /// /// - /// Finds and instantiates the subcommand using the arguments from the + /// 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. /// @@ -804,15 +804,15 @@ public IEnumerable GetCommands() /// /// 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. + /// invokes the method; otherwise, it invokes the + /// method on the command. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -836,18 +836,18 @@ public IEnumerable GetCommands() /// /// /// This method writes usage help for the application, including a list of all - /// subcommand names and their descriptions to . + /// subcommand names and their descriptions to . /// /// /// 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 + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -869,11 +869,11 @@ public void WriteUsage() /// and the description is retrieved from its attribute. /// /// - /// Commands that don't meet the criteria of the + /// Commands that don't meet the criteria of the /// predicate are not included. /// /// - /// If the is , only + /// 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 @@ -889,12 +889,12 @@ public string GetUsage() /// Gets the application description that will optionally be included in the usage help. /// /// - /// If the property is not , + /// 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(); diff --git a/src/Ookii.CommandLine/Commands/CommandOptions.cs b/src/Ookii.CommandLine/Commands/CommandOptions.cs index 5689eeeb..7c69c931 100644 --- a/src/Ookii.CommandLine/Commands/CommandOptions.cs +++ b/src/Ookii.CommandLine/Commands/CommandOptions.cs @@ -24,28 +24,28 @@ public class CommandOptions : ParseOptions /// /// /// 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 . + /// 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 and + /// indicated values, except that and /// 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 . + /// property to , the + /// property to , + /// the property to , + /// the property to , + /// the property to , + /// and the property to . /// /// public override bool IsPosix @@ -72,7 +72,7 @@ public override bool IsPosix /// /// /// One of the values of the enumeration. The default value - /// is . + /// is . /// public StringComparison CommandNameComparison { get; set; } = StringComparison.OrdinalIgnoreCase; @@ -82,7 +82,7 @@ public override bool IsPosix /// /// /// One of the values of the enumeration. The default value - /// is . + /// is . /// /// /// @@ -91,7 +91,7 @@ public override bool IsPosix /// specified transformation. /// /// - /// If this property is not , the value specified by the + /// 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. /// @@ -116,12 +116,12 @@ public override bool IsPosix /// /// /// This property is only used if the property is not - /// , and is never used for commands with an explicit + /// , 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 + /// and the default value of "Command" for this /// property, the name of the command will be "create-file" without having to explicitly /// specify it. /// diff --git a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs index f9cd133b..f4880213 100644 --- a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs +++ b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs @@ -8,7 +8,7 @@ namespace Ookii.CommandLine.Commands; /// /// /// This interface adds a method to the -/// interface, that will be invoked by the +/// interface, that will be invoked by the /// method and its overloads. This allows you to write tasks that use asynchronous code. /// /// @@ -27,9 +27,9 @@ public interface IAsyncCommand : ICommand /// command that was executed. /// /// - /// This method will only be invoked if you run commands with the + /// 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 + /// method to invoke this task. Use the /// class for a default implementation that does this. /// /// diff --git a/src/Ookii.CommandLine/Commands/ParentCommand.cs b/src/Ookii.CommandLine/Commands/ParentCommand.cs index c5de8349..20642168 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommand.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommand.cs @@ -168,14 +168,14 @@ protected virtual void OnChildCommandNotFound(string? commandName, CommandManage } /// - /// Method called when the property is set to - /// and a duplicate argument value was encountered. + /// Method called when the property is set to + /// and a duplicate argument value was encountered. /// /// The duplicate argument. /// The new value for the argument. /// /// - /// The base class implementation writes a warning to the + /// The base class implementation writes a warning to the /// writer. /// /// diff --git a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs index de31cd94..e67673ed 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs @@ -10,7 +10,7 @@ namespace Ookii.CommandLine.Commands; /// /// If you wish to have a command with nested subcommands, apply this attribute to the nested /// subcommand classes. The class will only return commands whose -/// property value matches the +/// property value matches the /// property. /// /// diff --git a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs index 0b3b0b59..e4f8241d 100644 --- a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs +++ b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs @@ -45,7 +45,7 @@ public class KeyValuePairConverter : ArgumentConverter /// /// /// Provides an optional custom key/value separator. If , the value - /// of is used. + /// of is used. /// /// /// Indicates whether the type of the pair's value accepts values. diff --git a/src/Ookii.CommandLine/Conversion/ParsableConverter.cs b/src/Ookii.CommandLine/Conversion/ParsableConverter.cs index ffc8e5ec..b009ac13 100644 --- a/src/Ookii.CommandLine/Conversion/ParsableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ParsableConverter.cs @@ -11,7 +11,7 @@ namespace Ookii.CommandLine.Conversion; /// The type to convert. /// /// -/// Conversion is performed using the method. +/// Conversion is performed using the method. /// /// /// Only use this converter for types that implement , but not diff --git a/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs index 995868bd..b00d1620 100644 --- a/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs @@ -11,7 +11,7 @@ namespace Ookii.CommandLine.Conversion; /// The type to convert. /// /// -/// Conversion is performed using the method. +/// Conversion is performed using the method. /// /// /// For types that implement , but not , diff --git a/src/Ookii.CommandLine/DescriptionListFilterMode.cs b/src/Ookii.CommandLine/DescriptionListFilterMode.cs index 8bdd1a85..567ac9fb 100644 --- a/src/Ookii.CommandLine/DescriptionListFilterMode.cs +++ b/src/Ookii.CommandLine/DescriptionListFilterMode.cs @@ -3,7 +3,7 @@ /// /// Indicates which arguments should be included in the description list when printing usage. /// -/// +/// public enum DescriptionListFilterMode { /// diff --git a/src/Ookii.CommandLine/DescriptionListSortMode.cs b/src/Ookii.CommandLine/DescriptionListSortMode.cs index 9972d670..0a565cf7 100644 --- a/src/Ookii.CommandLine/DescriptionListSortMode.cs +++ b/src/Ookii.CommandLine/DescriptionListSortMode.cs @@ -3,7 +3,7 @@ /// /// Indicates how the arguments in the description list should be sorted. /// -/// +/// public enum DescriptionListSortMode { /// @@ -14,7 +14,7 @@ public enum DescriptionListSortMode 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 + /// 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, @@ -25,7 +25,7 @@ public enum DescriptionListSortMode /// /// 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 . + /// , this has the same effect as . /// AlphabeticalShortName, /// diff --git a/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs b/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs index 4cd6c4ea..14f55487 100644 --- a/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs +++ b/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs @@ -3,7 +3,7 @@ namespace Ookii.CommandLine; /// -/// Provides data for the event. +/// Provides data for the event. /// public class DuplicateArgumentEventArgs : EventArgs { diff --git a/src/Ookii.CommandLine/ErrorMode.cs b/src/Ookii.CommandLine/ErrorMode.cs index 197621b6..f7fcd05d 100644 --- a/src/Ookii.CommandLine/ErrorMode.cs +++ b/src/Ookii.CommandLine/ErrorMode.cs @@ -3,7 +3,7 @@ /// /// Indicates whether something is an error, warning, or allowed. /// -/// +/// public enum ErrorMode { /// diff --git a/src/Ookii.CommandLine/IParser.cs b/src/Ookii.CommandLine/IParser.cs index 6dff2d22..5d3f4b00 100644 --- a/src/Ookii.CommandLine/IParser.cs +++ b/src/Ookii.CommandLine/IParser.cs @@ -29,7 +29,7 @@ public interface IParser : IParserProvider where TSelf : class, IParser { /// - /// Parses the arguments returned by the + /// Parses the arguments returned by the /// method using the type . /// /// @@ -38,10 +38,10 @@ public interface IParser : IParserProvider /// /// /// An instance of the type , or if an - /// error occurred, or argument parsing was canceled by the + /// error occurred, or argument parsing was canceled by the /// property or a method argument that returned . /// - /// + /// public static abstract TSelf? Parse(ParseOptions? options = null); /// @@ -54,10 +54,10 @@ public interface IParser : IParserProvider /// /// /// An instance of the type , or if an - /// error occurred, or argument parsing was canceled by the + /// error occurred, or argument parsing was canceled by the /// property or a method argument that returned . /// - /// + /// public static abstract TSelf? Parse(string[] args, ParseOptions? options = null); /// @@ -71,10 +71,10 @@ public interface IParser : IParserProvider /// /// /// An instance of the type , or if an - /// error occurred, or argument parsing was canceled by the + /// error occurred, or argument parsing was canceled by the /// property or a method argument that returned . /// - /// + /// public static abstract TSelf? Parse(ReadOnlyMemory args, ParseOptions? options = null); } diff --git a/src/Ookii.CommandLine/IParserProvider.cs b/src/Ookii.CommandLine/IParserProvider.cs index b788f375..d5bafb1c 100644 --- a/src/Ookii.CommandLine/IParserProvider.cs +++ b/src/Ookii.CommandLine/IParserProvider.cs @@ -45,7 +45,7 @@ public interface IParserProvider /// names or positions. Even when the parser was generated using the /// class, not all those rules can be checked at compile time. /// - /// + /// public static abstract CommandLineParser CreateParser(ParseOptions? options = null); } diff --git a/src/Ookii.CommandLine/LineWrappingTextWriter.cs b/src/Ookii.CommandLine/LineWrappingTextWriter.cs index 303fe627..feab9be1 100644 --- a/src/Ookii.CommandLine/LineWrappingTextWriter.cs +++ b/src/Ookii.CommandLine/LineWrappingTextWriter.cs @@ -314,8 +314,8 @@ struct NoWrappingState /// 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 + /// 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. /// @@ -414,15 +414,15 @@ public int Indent /// /// /// One of the values of the enumeration. If no maximum line - /// length is set, the value is always . + /// length is set, the value is always . /// /// /// - /// When this property is changed to the buffer will + /// When this property is changed to the buffer will /// be flushed synchronously if not empty. /// /// - /// When this property is changed from to another + /// 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. /// @@ -459,7 +459,7 @@ public WrappingMode Wrapping /// /// Gets a that writes to the standard output stream, - /// using as the maximum line length. + /// using as the maximum line length. /// /// A that writes to the standard output stream. public static LineWrappingTextWriter ForConsoleOut() @@ -469,7 +469,7 @@ public static LineWrappingTextWriter ForConsoleOut() /// /// Gets a that writes to the standard error stream, - /// using as the maximum line length. + /// using as the maximum line length. /// /// A that writes to the standard error stream. public static LineWrappingTextWriter ForConsoleError() diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs b/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs index acf8c150..29a4ec6f 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs @@ -8,7 +8,7 @@ namespace Ookii.CommandLine; public partial class LocalizedStringProvider { /// - /// Gets the error message for . + /// Gets the error message for . /// /// The error message. /// @@ -20,7 +20,7 @@ public partial class LocalizedStringProvider public virtual string UnspecifiedError() => Resources.UnspecifiedError; /// - /// Gets the error message for . + /// Gets the error message for . /// /// The name of the argument. /// The value of the argument. @@ -30,14 +30,14 @@ public virtual string ArgumentValueConversionError(string argumentName, string? => Format(Resources.ArgumentConversionErrorFormat, argumentValue, argumentName, valueDescription); /// - /// Gets the error message for . + /// 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 . + /// Gets the error message for . /// /// The name of the argument. /// The error message. @@ -45,28 +45,28 @@ public virtual string MissingNamedArgumentValue(string argumentName) => Format(Resources.MissingValueForNamedArgumentFormat, argumentName); /// - /// Gets the error message for . + /// 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 . + /// 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 . + /// Gets the error message for . /// /// The error message. public virtual string TooManyArguments() => Resources.TooManyArguments; /// - /// Gets the error message for . + /// Gets the error message for . /// /// The name of the argument. /// The error message. @@ -74,7 +74,7 @@ public virtual string MissingRequiredArgument(string argumentName) => Format(Resources.MissingRequiredArgumentFormat, argumentName); /// - /// Gets the error message for . + /// Gets the error message for . /// /// The name of the argument. /// The value of the argument. @@ -84,7 +84,7 @@ public virtual string InvalidDictionaryValue(string argumentName, string? argume => Format(Resources.InvalidDictionaryValueFormat, argumentName, argumentValue, message); /// - /// Gets the error message for . + /// Gets the error message for . /// /// The error message of the conversion. /// The error message. @@ -92,7 +92,7 @@ public virtual string CreateArgumentsTypeError(string? message) => Format(Resources.CreateArgumentsTypeErrorFormat, message); /// - /// Gets the error message for . + /// Gets the error message for . /// /// The name of the argument. /// The error message of the conversion. @@ -101,14 +101,14 @@ public virtual string ApplyValueError(string argumentName, string? message) => Format(Resources.SetValueErrorFormat, argumentName, message); /// - /// Gets the error message for . + /// 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 . + /// Gets the error message for . /// /// The names of the combined short arguments. /// The error message. diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs b/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs index d67af141..44d602af 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs @@ -150,7 +150,7 @@ public virtual string RequiresAnyUsageHelp(IEnumerable argu } /// - /// Gets a generic error message for the base implementation of . + /// Gets a generic error message for the base implementation of . /// /// The name of the argument. /// The error message. @@ -158,7 +158,7 @@ public virtual string ValidationFailed(string argumentName) => Format(Resources.ValidationFailedFormat, argumentName); /// - /// Gets a generic error message for the base implementation of . + /// Gets a generic error message for the base implementation of . /// /// The error message. public virtual string ClassValidationFailed() => Resources.ClassValidationFailed; diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.cs b/src/Ookii.CommandLine/LocalizedStringProvider.cs index 6996ec66..73e9fd4a 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.cs @@ -11,7 +11,7 @@ namespace Ookii.CommandLine; /// /// /// Inherit from this class and override its members to provide customized or localized -/// strings. You can specify the implementation to use using . +/// strings. You can specify the implementation to use using . /// /// /// For error messages, this only lets you customize error messages for the @@ -23,14 +23,14 @@ namespace Ookii.CommandLine; public partial class LocalizedStringProvider { /// - /// Gets the name of the help argument created if the - /// or property is . + /// 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 + /// Gets the short name of the help argument created if the /// property is , typically '?'. /// /// The string. @@ -41,42 +41,42 @@ public partial class LocalizedStringProvider /// is the same according to the argument name comparer, then no alias is added. /// /// - /// If is not , + /// 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 + /// 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 + /// 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 + /// 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 + /// 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 + /// Gets the description of the version command created if the /// property is . /// /// The string. @@ -88,7 +88,7 @@ public partial class LocalizedStringProvider /// /// The assembly whose version to use. /// - /// The friendly name of the application; typically the value of the + /// The friendly name of the application; typically the value of the /// property. /// /// The string. diff --git a/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs b/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs index e26e2afd..ea22643f 100644 --- a/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs +++ b/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs @@ -28,7 +28,7 @@ namespace Ookii.CommandLine; /// positional arguments until another named argument has been supplied. /// /// -/// Using white-space separators will not work if the +/// Using white-space separators will not work if the /// is or if the argument is a multi-value switch argument. /// /// diff --git a/src/Ookii.CommandLine/NameTransform.cs b/src/Ookii.CommandLine/NameTransform.cs index 1b0d7eff..91f722ff 100644 --- a/src/Ookii.CommandLine/NameTransform.cs +++ b/src/Ookii.CommandLine/NameTransform.cs @@ -4,11 +4,11 @@ namespace Ookii.CommandLine; /// Indicates how to transform the property, parameter, or method name if an argument doesn't /// have an explicit name. /// -/// -/// -/// +/// +/// +/// /// -/// +/// public enum NameTransform { /// diff --git a/src/Ookii.CommandLine/NameTransformExtensions.cs b/src/Ookii.CommandLine/NameTransformExtensions.cs index e01fb4d1..a1ef0e53 100644 --- a/src/Ookii.CommandLine/NameTransformExtensions.cs +++ b/src/Ookii.CommandLine/NameTransformExtensions.cs @@ -15,7 +15,7 @@ public static class NameTransformExtensions /// The name to transform. /// /// An optional suffix to remove from the string before transformation. Only used if - /// is not . + /// is not . /// /// The transformed name. /// diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index feeb2ff8..06f4e7f9 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -9,7 +9,7 @@ namespace Ookii.CommandLine; /// -/// Provides options for the +/// Provides options for the /// method and the constructor. /// /// @@ -30,9 +30,9 @@ public class ParseOptions /// /// /// The culture used to convert command line argument values from their string representation to the argument type, or - /// to use . The default value is + /// to use . The default value is /// - /// + /// public CultureInfo? Culture { get; set; } /// @@ -40,7 +40,7 @@ public class ParseOptions /// representation to the argument type. /// /// - /// The value of the property, or + /// The value of the property, or /// if that property is . /// public CultureInfo CultureOrDefault => Culture ?? CultureInfo.InvariantCulture; @@ -51,23 +51,23 @@ public class ParseOptions /// /// 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 + /// attribute is not present, . The default value is /// . /// /// /// /// If not , this property overrides the value of the - /// property. + /// 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 + /// The value of the property, or /// if that property is . /// public ParsingMode ModeOrDefault => Mode ?? ParsingMode.Default; @@ -88,10 +88,10 @@ public class ParseOptions /// /// /// Setting this property to is equivalent to setting the - /// property to , the - /// property to , - /// the property to , - /// and the property to . + /// property to , the + /// property to , + /// the property to , + /// and the property to . /// /// /// This property will only return if the above properties are the @@ -101,14 +101,14 @@ public class ParseOptions /// /// /// Setting this property to is equivalent to setting the - /// property to , the - /// property to , - /// the property to , - /// and the property to . + /// property to , the + /// property to , + /// the property to , + /// and the property to . /// /// - /// - /// + /// + /// public virtual bool IsPosix { get => Mode == ParsingMode.LongShort && ArgumentNameComparisonOrDefault.IsCaseSensitive() && @@ -139,12 +139,12 @@ public virtual bool IsPosix /// /// 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 + /// attribute is not present, . The default value is /// . /// /// /// - /// If an argument doesn't have the + /// 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 transform. /// @@ -154,12 +154,12 @@ public virtual bool IsPosix /// /// /// If not , this property overrides the value of the - /// property. + /// property. /// /// /// /// - /// + /// public NameTransform? ArgumentNameTransform { get; set; } /// @@ -167,7 +167,7 @@ public virtual bool IsPosix /// name. /// /// - /// The value of the property, or + /// The value of the property, or /// if that property is . /// public NameTransform ArgumentNameTransformOrDefault => ArgumentNameTransform ?? NameTransform.None; @@ -178,22 +178,22 @@ public virtual bool IsPosix /// /// 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 + /// the current platform as returned by the /// method. The default value is . /// /// /// - /// If the parsing mode is set to , either using the + /// 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. + /// property. /// /// - /// + /// public IEnumerable? ArgumentNamePrefixes { get; set; } @@ -202,7 +202,7 @@ public virtual bool IsPosix /// /// /// The value of the property, or the return value of the - /// method if that property + /// method if that property /// is /// public IEnumerable ArgumentNamePrefixesOrDefault => ArgumentNamePrefixes ?? CommandLineParser.GetDefaultArgumentNamePrefixes(); @@ -213,12 +213,12 @@ public virtual bool IsPosix /// /// The long argument prefix, or to use the value from the /// attribute, or if not set, the default prefix from - /// the constant. The default + /// the constant. The default /// value is . /// /// /// - /// This property is only used if the if the parsing mode is set to , + /// This property is only used if the if the parsing mode is set to , /// either using the property or the /// attribute /// @@ -228,10 +228,10 @@ public virtual bool IsPosix /// /// /// If not , this property overrides the value of the - /// property. + /// property. /// /// - /// + /// public string? LongArgumentNamePrefix { get; set; } /// @@ -239,7 +239,7 @@ public virtual bool IsPosix /// /// /// The value of the property, or the value of the - /// constant if that property + /// constant if that property /// is /// public string LongArgumentNamePrefixOrDefault => LongArgumentNamePrefix ?? CommandLineParser.DefaultLongArgumentNamePrefix; @@ -250,25 +250,25 @@ public virtual bool IsPosix /// /// One of the values of the enumeration, or /// to use the one determined using the - /// property, or if the + /// property, or if the /// is not present, - /// . The default value is + /// . The default value is /// . /// /// /// /// If not , this property overrides the value of the - /// property. + /// property. /// /// - /// + /// public StringComparison? ArgumentNameComparison { get; set; } /// /// Gets the type of string comparison to use for argument names. /// /// - /// The value of the property, or + /// The value of the property, or /// if that property is . /// public StringComparison ArgumentNameComparisonOrDefault => ArgumentNameComparison ?? StringComparison.OrdinalIgnoreCase; @@ -284,7 +284,7 @@ public virtual bool IsPosix /// /// The used to print error information, or /// to print to a for the standard error stream - /// (). The default value is . + /// (). The default value is . /// public TextWriter? Error { get; set; } @@ -294,36 +294,36 @@ public virtual bool IsPosix /// /// 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 + /// 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 + /// 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 + /// 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 + /// is identical to and no /// warning is displayed. /// /// /// If not , this property overrides the value of the - /// property. + /// property. /// /// - /// + /// public ErrorMode? DuplicateArguments { get; set; } /// /// Gets a value indicating whether duplicate arguments are allowed. /// /// - /// The value of the property, or + /// The value of the property, or /// if that property is . /// public ErrorMode DuplicateArgumentsOrDefault => DuplicateArguments ?? ErrorMode.Error; @@ -334,17 +334,17 @@ public virtual bool IsPosix /// /// 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 + /// 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. + /// property. /// /// - /// + /// public bool? AllowWhiteSpaceValueSeparator { get; set; } /// @@ -363,7 +363,7 @@ public virtual bool IsPosix /// /// 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 + /// is not present, the values returned by the /// method, which are a colon (:) and an equals sign (=). The default value is . /// /// @@ -387,7 +387,7 @@ public virtual bool IsPosix /// /// /// If not , this property overrides the value of the - /// property. + /// property. /// /// public IEnumerable? NameValueSeparators { get; set; } @@ -397,7 +397,7 @@ public virtual bool IsPosix /// /// /// The value of the property, or the return value of the - /// method if that property is + /// method if that property is /// . /// public IEnumerable NameValueSeparatorsOrDefault => NameValueSeparators ?? CommandLineParser.GetDefaultNameValueSeparators(); @@ -414,7 +414,7 @@ public virtual bool IsPosix /// /// /// If this property is , the - /// will automatically add an argument with the name "Help". If using , + /// 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. @@ -429,12 +429,12 @@ public virtual bool IsPosix /// /// /// If not , this property overrides the value of the - /// property. + /// property. /// /// - /// - /// - /// + /// + /// + /// public bool? AutoHelpArgument { get; set; } /// @@ -452,7 +452,7 @@ public virtual bool IsPosix /// /// to automatically create a version argument; /// to not create one, or to use the value from the - /// property, or if the + /// property, or if the /// is not present, . /// The default value is . /// @@ -472,11 +472,11 @@ public virtual bool IsPosix /// /// /// If not , this property overrides the value of the - /// property. + /// property. /// /// - /// - /// + /// + /// public bool? AutoVersionArgument { get; set; } /// @@ -496,7 +496,7 @@ public virtual bool IsPosix /// /// 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 + /// to use the value from the /// property, or if the attribute is not present, /// . /// @@ -510,17 +510,17 @@ public virtual bool IsPosix /// doesn't uniquely identify a single argument. /// /// - /// When using , this only applies to long names. Explicit + /// 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. + /// property. /// /// /// If not , this property overrides the value of the - /// property. + /// property. /// /// public bool? AutoPrefixAliases { get; set; } @@ -540,7 +540,7 @@ public virtual bool IsPosix /// /// /// The virtual terminal sequence for a color. The default value is - /// . + /// . /// /// /// @@ -553,7 +553,7 @@ public virtual bool IsPosix /// . /// /// - /// After the error message, the value of the + /// After the error message, the value of the /// property will be written to undo the color change. /// /// @@ -564,7 +564,7 @@ public virtual bool IsPosix /// /// /// The virtual terminal sequence for a color. The default value is - /// . + /// . /// /// /// @@ -573,7 +573,7 @@ public virtual bool IsPosix /// /// /// This color is used for the warning emitted if the - /// property is . + /// property is . /// /// /// If the string contains anything other than virtual terminal sequences, those parts @@ -581,7 +581,7 @@ public virtual bool IsPosix /// . /// /// - /// After the warning message, the value of the + /// After the warning message, the value of the /// property will be written to undo the color change. /// /// @@ -597,10 +597,10 @@ public virtual bool IsPosix /// /// /// If this property is and the property is - /// , the - /// method, the + /// , the + /// method, the /// method and the class will determine if color is supported - /// using the method for the standard error + /// using the method for the standard error /// stream. /// /// @@ -626,7 +626,7 @@ public virtual bool IsPosix /// strings. /// /// - /// + /// public LocalizedStringProvider StringProvider { get => _stringProvider ??= new LocalizedStringProvider(); @@ -638,15 +638,15 @@ public LocalizedStringProvider StringProvider /// /// /// One of the values of the enumeration. The default value - /// is . + /// is . /// /// /// - /// If the value of this property is not , the - /// method, the - /// method and the + /// 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. + /// method instead of usage help. /// /// public UsageHelpRequest ShowUsageOnError { get; set; } @@ -670,7 +670,7 @@ public LocalizedStringProvider StringProvider /// the transformation specified by the property. /// /// - /// + /// public IDictionary? DefaultValueDescriptions { get; set; } /// @@ -680,16 +680,16 @@ public LocalizedStringProvider StringProvider /// /// 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 . + /// not present, . The default value is . /// /// /// /// This property has no effect on explicit value description specified with the - /// property or the + /// property or the /// property. /// /// - /// If not , this property overrides the + /// If not , this property overrides the /// property. /// /// @@ -699,7 +699,7 @@ public LocalizedStringProvider StringProvider /// Gets a value that indicates how value descriptions derived from type names are transformed. /// /// - /// The value of the property, or + /// The value of the property, or /// if that property is . /// public NameTransform ValueDescriptionTransformOrDefault => ValueDescriptionTransform ?? NameTransform.None; @@ -718,7 +718,7 @@ public LocalizedStringProvider StringProvider /// /// This property only applies when you manually construct an instance of the /// or class, or use one - /// of the static methods. If you use + /// of the static methods. If you use /// the generated static CreateParser and Parse methods on the command line /// arguments type, the generated parser is used regardless of the value of this property. /// diff --git a/src/Ookii.CommandLine/ParseOptionsAttribute.cs b/src/Ookii.CommandLine/ParseOptionsAttribute.cs index 3a6cc008..1faea566 100644 --- a/src/Ookii.CommandLine/ParseOptionsAttribute.cs +++ b/src/Ookii.CommandLine/ParseOptionsAttribute.cs @@ -33,15 +33,15 @@ public class ParseOptionsAttribute : Attribute /// Gets or sets a value that indicates the command line argument parsing rules to use. /// /// - /// The to use. The default is . + /// The to use. The default is . /// /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// - /// + /// public ParsingMode Mode { get; set; } /// @@ -60,10 +60,10 @@ public class ParseOptionsAttribute : Attribute /// /// /// Setting this property to is equivalent to setting the - /// property to , the + /// property to , the /// property to , - /// the property to , - /// and the property to . + /// the property to , + /// and the property to . /// /// /// This property will only return if the above properties are the @@ -72,13 +72,13 @@ public class ParseOptionsAttribute : Attribute /// /// /// Setting this property to is equivalent to setting the - /// property to , the + /// property to , the /// property to , - /// the property to , - /// and the property to . + /// the property to , + /// and the property to . /// /// - /// + /// public virtual bool IsPosix { get => Mode == ParsingMode.LongShort && CaseSensitive && ArgumentNameTransform == NameTransform.DashCase && @@ -108,11 +108,11 @@ public virtual bool IsPosix /// /// /// One of the values of the enumeration. The default value is - /// . + /// . /// /// /// - /// If an argument doesn't have the + /// 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. /// @@ -121,7 +121,7 @@ public virtual bool IsPosix /// help and version attributes. /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// @@ -133,22 +133,22 @@ public virtual bool IsPosix /// /// /// An array of prefixes, or to use the value of - /// . The default value is + /// . The default value is /// /// /// /// - /// If the property is , - /// or if the parsing mode is set to + /// 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 + /// This value can be overridden by the /// property. /// /// - /// + /// public string[]? ArgumentNamePrefixes { get; set; } /// @@ -157,19 +157,19 @@ public virtual bool IsPosix /// /// /// This property is only used if the property is - /// , or if the parsing mode is set to - /// elsewhere. + /// , 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 + /// This value can be overridden by the /// property. /// /// - /// + /// public string? LongArgumentNamePrefix { get; set; } /// @@ -184,15 +184,15 @@ public virtual bool IsPosix /// /// /// When , the will use - /// for command line argument comparisons; otherwise, - /// it will use . + /// for command line argument comparisons; otherwise, + /// it will use . /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// - /// + /// public bool CaseSensitive { get; set; } /// @@ -200,28 +200,28 @@ public virtual bool IsPosix /// /// /// 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 + /// 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 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 + /// This value can be overridden by the /// property. /// /// - /// + /// public ErrorMode DuplicateArguments { get; set; } /// @@ -235,11 +235,11 @@ public virtual bool IsPosix /// /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// - /// + /// public bool AllowWhiteSpaceValueSeparator { get; set; } = true; /// @@ -247,7 +247,7 @@ public virtual bool IsPosix /// /// /// The characters used to separate the name and the value of an argument, or - /// to use the default value from the + /// to use the default value from the /// method, which is a colon ':' and an equals sign '='. The default value is . /// /// @@ -270,11 +270,11 @@ public virtual bool IsPosix /// is allowed as a separator. /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// - /// + /// public char[]? NameValueSeparators { get; set; } /// @@ -287,7 +287,7 @@ public virtual bool IsPosix /// /// /// If this property is , the - /// will automatically add an argument with the name "Help". If using , + /// 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. @@ -301,13 +301,13 @@ public virtual bool IsPosix /// The name, aliases and description can be customized by using a custom . /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// - /// - /// - /// + /// + /// + /// public bool AutoHelpArgument { get; set; } = true; /// @@ -335,12 +335,12 @@ public virtual bool IsPosix /// The name and description can be customized by using a custom . /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// - /// - /// + /// + /// public bool AutoVersionArgument { get; set; } = true; /// @@ -362,16 +362,16 @@ public virtual bool IsPosix /// doesn't uniquely identify a single argument. /// /// - /// When using , this only applies to long names. Explicit + /// 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. + /// property. /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// @@ -383,20 +383,20 @@ public virtual bool IsPosix /// /// /// One of the members of the enumeration. The default value is - /// . + /// . /// /// /// /// This property has no effect on explicit value description specified with the - /// property or the - /// property. + /// property or the + /// property. /// /// - /// This value can be overridden by the + /// This value can be overridden by the /// property. /// /// - /// + /// public NameTransform ValueDescriptionTransform { get; set; } internal StringComparison GetStringComparison() diff --git a/src/Ookii.CommandLine/ParseResult.cs b/src/Ookii.CommandLine/ParseResult.cs index 0e599ba1..9165ed18 100644 --- a/src/Ookii.CommandLine/ParseResult.cs +++ b/src/Ookii.CommandLine/ParseResult.cs @@ -3,10 +3,10 @@ namespace Ookii.CommandLine; /// -/// Indicates the result of the last call to the +/// Indicates the result of the last call to the /// method. /// -/// +/// public readonly struct ParseResult { private ParseResult(ParseStatus status, CommandLineArgumentException? exception = null, string? argumentName = null, @@ -19,7 +19,7 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception } /// - /// Gets the status of the last call to the + /// Gets the status of the last call to the /// method. /// /// @@ -29,7 +29,7 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception /// /// Gets the exception that occurred during the last call to the - /// method, if any. + /// method, if any. /// /// /// The exception, or if parsing was successful or canceled. @@ -40,9 +40,9 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception /// 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. + /// 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; } @@ -57,7 +57,7 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception /// /// /// - /// If parsing succeeded without encountering an argument using , + /// If parsing succeeded without encountering an argument using , /// this collection will always be empty. /// /// @@ -79,7 +79,7 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception /// Gets a instance that represents successful parsing. /// /// - /// The name of the argument that canceled parsing using . + /// The name of the argument that canceled parsing using . /// /// Any remaining arguments that were not parsed. /// diff --git a/src/Ookii.CommandLine/ParseStatus.cs b/src/Ookii.CommandLine/ParseStatus.cs index 38b58c15..988c7442 100644 --- a/src/Ookii.CommandLine/ParseStatus.cs +++ b/src/Ookii.CommandLine/ParseStatus.cs @@ -1,14 +1,14 @@ namespace Ookii.CommandLine; /// -/// Indicates the status of the last call to the +/// Indicates the status of the last call to the /// method. /// /// public enum ParseStatus { /// - /// The method has not been called yet. + /// The method has not been called yet. /// None, /// diff --git a/src/Ookii.CommandLine/ParsingMode.cs b/src/Ookii.CommandLine/ParsingMode.cs index 29c6e468..f38dfaae 100644 --- a/src/Ookii.CommandLine/ParsingMode.cs +++ b/src/Ookii.CommandLine/ParsingMode.cs @@ -5,11 +5,11 @@ /// /// /// -/// To set the parsing mode for a , use the -/// property or the property. +/// To set the parsing mode for a , use the +/// property or the property. /// /// -/// +/// public enum ParsingMode { /// @@ -17,8 +17,8 @@ public enum ParsingMode /// Default, /// - /// Allow arguments to have both long and short names, using the - /// to specify a long name, and the regular + /// 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/ShortAliasAttribute.cs b/src/Ookii.CommandLine/ShortAliasAttribute.cs index 779aa058..38fcc79b 100644 --- a/src/Ookii.CommandLine/ShortAliasAttribute.cs +++ b/src/Ookii.CommandLine/ShortAliasAttribute.cs @@ -10,10 +10,10 @@ namespace Ookii.CommandLine; /// 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 -/// . +/// 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 @@ -24,8 +24,8 @@ namespace Ookii.CommandLine; /// unique. /// /// -/// By default, the command line usage help generated by -/// includes the aliases. Set the +/// By default, the command line usage help generated by +/// includes the aliases. Set the /// property to to exclude them. /// /// diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs index a37f93c4..a58acdb1 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -115,7 +115,7 @@ public void RunValidators(CommandLineParser parser) /// /// 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 . public abstract object CreateInstance(CommandLineParser parser, object?[]? requiredPropertyValues); diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 6e41eb7b..70bebb0d 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -54,7 +54,7 @@ private GeneratedArgument(ArgumentInfo info, Action? setPropert /// Indicates if the argument used a C# 11 required property. /// /// - /// Default value to use if the property + /// Default value to use if the property /// is not set. /// /// The type of the key of a dictionary argument. diff --git a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs index 68264191..366de22e 100644 --- a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs +++ b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs @@ -25,7 +25,7 @@ public static class VirtualTerminal /// The to enable VT sequences for. /// /// An instance of the class that will disable - /// virtual terminal support when disposed or destructed. Use the + /// virtual terminal support when disposed or destructed. Use the /// property to check if virtual terminal sequences are supported. /// /// @@ -36,7 +36,7 @@ public static class VirtualTerminal /// environment variable is defined. /// /// - /// For , this method does nothing and always returns + /// For , this method does nothing and always returns /// . /// /// @@ -82,7 +82,7 @@ public static VirtualTerminalSupport EnableVirtualTerminalSequences(StandardStre /// The to enable color sequences for. /// /// An instance of the class that will disable - /// virtual terminal support when disposed or destructed. Use the + /// virtual terminal support when disposed or destructed. Use the /// property to check if virtual terminal sequences are supported. /// /// diff --git a/src/Ookii.CommandLine/UsageHelpRequest.cs b/src/Ookii.CommandLine/UsageHelpRequest.cs index 935001ee..5fdc8cf5 100644 --- a/src/Ookii.CommandLine/UsageHelpRequest.cs +++ b/src/Ookii.CommandLine/UsageHelpRequest.cs @@ -3,12 +3,12 @@ /// /// Indicates if and how usage is shown if an error occurred parsing the command line. /// -/// +/// public enum UsageHelpRequest { /// /// Only the usage syntax is shown; the argument descriptions are not. In addition, the - /// message is shown. + /// message is shown. /// SyntaxOnly, /// @@ -16,7 +16,7 @@ public enum UsageHelpRequest /// Full, /// - /// No usage help is shown. Instead, the + /// 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 dfbdc6b1..d5dc1aaf 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -12,13 +12,13 @@ namespace Ookii.CommandLine; /// -/// Creates usage help for the class and the +/// 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. +/// help. Set the property to specify a custom instance. /// /// /// Depending on what methods you override, you can change small parts of the formatting, or @@ -67,7 +67,7 @@ protected enum Operation public const int DefaultSyntaxIndent = 3; /// - /// The default indentation for the argument descriptions for the + /// The default indentation for the argument descriptions for the /// mode. /// /// @@ -115,14 +115,14 @@ protected enum Operation /// to enable color output using virtual terminal sequences; /// to disable it; or, to automatically /// enable it if is using the - /// method. + /// 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 . + /// be wrapped, depending on the value returned by . /// /// public UsageWriter(LineWrappingTextWriter? writer = null, bool? useColor = null) @@ -132,11 +132,11 @@ public UsageWriter(LineWrappingTextWriter? writer = null, bool? useColor = null) } /// - /// Gets or sets a value indicating whether the value of the property + /// Gets or sets a value indicating whether the value of the property /// is written before the syntax. /// /// - /// if the value of the property + /// if the value of the property /// is written before the syntax; otherwise, . The default value is . /// public bool IncludeApplicationDescription { get; set; } = true; @@ -166,7 +166,7 @@ public UsageWriter(LineWrappingTextWriter? writer = null, bool? useColor = null) /// /// /// The application executable name, or to use the default value, - /// determined by calling . + /// determined by calling . /// /// #if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER @@ -189,7 +189,7 @@ public virtual string ExecutableName /// /// /// If the property is , the executable - /// name is determined by calling , + /// name is determined by calling , /// passing the value of this property as the argument. /// /// @@ -212,7 +212,7 @@ public bool IncludeExecutableExtension /// /// /// The virtual terminal sequence for a color. The default value is - /// . + /// . /// /// /// @@ -267,8 +267,8 @@ public bool IncludeExecutableExtension /// /// /// - /// This property is only used when the property is - /// . + /// This property is only used when the property is + /// . /// /// public bool UseShortNamesForSyntax { get; set; } @@ -297,8 +297,8 @@ public bool IncludeExecutableExtension /// /// 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 - /// . + /// argument's description, if the property is + /// . /// /// /// The number of characters by which to indent the argument descriptions. The default @@ -321,7 +321,7 @@ public bool IncludeExecutableExtension /// /// /// One of the values of the enumeration. The default - /// value is . + /// value is . /// public DescriptionListFilterMode ArgumentDescriptionListFilter { get; set; } @@ -331,7 +331,7 @@ public bool IncludeExecutableExtension ///
/// /// One of the values of the enumeration. The default - /// value is . + /// value is . /// public DescriptionListSortMode ArgumentDescriptionListOrder { get; set; } @@ -340,7 +340,7 @@ public bool IncludeExecutableExtension ///
/// /// The virtual terminal sequence for a color. The default value is - /// . + /// . /// /// /// @@ -365,7 +365,7 @@ public bool IncludeExecutableExtension /// /// Gets or sets a value indicating whether white space, rather than the first item of the - /// property, is used to separate + /// property, is used to separate /// arguments and their values in the command line syntax. /// /// @@ -379,7 +379,7 @@ public bool IncludeExecutableExtension /// 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 + /// The command line syntax will only use a white space character as the value separator if both the property /// and the property are true. /// /// @@ -414,7 +414,7 @@ public bool IncludeExecutableExtension public bool IncludeDefaultValueInDescription { get; set; } = true; /// - /// Gets or sets a value indicating whether the + /// Gets or sets a value indicating whether the /// attributes of an argument should be included in the argument description. /// /// @@ -444,7 +444,7 @@ public bool IncludeExecutableExtension ///
/// /// The virtual terminal sequence used to reset color. The default value is - /// . + /// . /// /// /// @@ -488,7 +488,7 @@ public bool IncludeExecutableExtension ///
/// /// The virtual terminal sequence for a color. The default value is - /// . + /// . /// /// /// @@ -546,15 +546,15 @@ public bool IncludeExecutableExtension /// /// /// If this property is , the instruction will be shown under the - /// following conditions: the property is + /// following conditions: the property is /// or ; for every command with a - /// attribute, the + /// 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; and - /// every command uses the same values for the - /// and properties. + /// no command specifies custom values for the + /// and properties; and + /// every command uses the same values for the + /// and properties. /// /// /// If set to , the message is shown even if not all commands @@ -582,7 +582,7 @@ public bool IncludeExecutableExtension /// assembly has no description, nothing is written. /// /// - /// If the property is not , + /// If the property is not , /// and the specified type has a , that description is /// used instead. /// @@ -604,8 +604,8 @@ public bool IncludeExecutableExtension ///
/// /// The passed to the - /// constructor, or an instance created by the - /// or + /// constructor, or an instance created by the + /// or /// function. /// /// @@ -641,15 +641,15 @@ protected CommandManager CommandManager /// /// /// - /// If this property is not , the + /// If this property is not , the /// property will throw an exception. /// /// - /// If this property is not , the + /// If this property is not , the /// property will throw an exception. /// /// - /// If this property is , the + /// If this property is , the /// property may throw an exception. /// /// @@ -704,7 +704,7 @@ protected Operation OperationInProgress /// 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 . + /// if the output supports it according to . /// /// /// This method calls the method to create the usage help @@ -720,7 +720,7 @@ public void WriteParserUsage(CommandLineParser parser, UsageHelpRequest request /// /// Creates usage help for the specified command manager. /// - /// The + /// The /// /// is . /// @@ -732,7 +732,7 @@ public void WriteParserUsage(CommandLineParser parser, UsageHelpRequest request /// 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 . + /// if the output supports it according to . /// /// /// This method calls the method to create the @@ -760,7 +760,7 @@ public void WriteCommandListUsage(CommandManager manager) /// /// /// This method ignores the writer passed to the - /// constructor, and will use the + /// constructor, and will use the /// method instead, and returns the resulting string. If color support was not explicitly /// enabled, it will be disabled. /// @@ -779,7 +779,7 @@ public string GetUsage(CommandLineParser parser, UsageHelpRequest request = Usag /// Returns a string with usage help for the specified command manager. ///
/// A string containing the usage help. - /// The + /// The /// /// The length at which to white-space wrap lines in the output, or 0 to disable wrapping. /// @@ -792,7 +792,7 @@ public string GetUsage(CommandLineParser parser, UsageHelpRequest request = Usag /// /// /// This method ignores the writer passed to the - /// constructor, and will use the + /// constructor, and will use the /// method instead, and returns the resulting string. If color support was not explicitly /// enabled, it will be disabled. /// @@ -1079,7 +1079,7 @@ protected virtual void WriteArgumentSyntax(CommandLineArgument argument) ///
/// The name of the argument. /// - /// The argument name prefix; if using , this may vary + /// The argument name prefix; if using , this may vary /// depending on whether the name is a short or long name. /// /// @@ -1102,12 +1102,12 @@ protected virtual void WriteArgumentName(string argumentName, string prefix) ///
/// The name of the argument. /// - /// The argument name prefix; if using , this may vary + /// 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 + /// property and the property /// are both . /// /// @@ -1417,7 +1417,7 @@ protected virtual void WriteArgumentDescriptionBody(CommandLineArgument argument ///
/// The argument name or alias. /// - /// The argument name prefix; if using , this may vary + /// The argument name prefix; if using , this may vary /// depending on whether the name or alias is a short or long name or alias. /// /// @@ -1478,7 +1478,7 @@ protected virtual void WriteSwitchValueDescription(string valueDescription) /// Writes the aliases of an argument for use in the argument description list. ///
/// - /// The aliases of an argument, or the long aliases for + /// The aliases of an argument, or the long aliases for /// mode, or if the argument has no (long) aliases. /// /// @@ -1526,7 +1526,7 @@ protected virtual void WriteAliases(IEnumerable? aliases, IEnumerable /// The alias. /// - /// The argument name prefix; if using , this may vary + /// The argument name prefix; if using , this may vary /// depending on whether the alias is a short or long alias. /// /// @@ -1600,7 +1600,7 @@ protected virtual void WriteArgumentValidators(CommandLineArgument argument) /// /// This method is called by the base implementation of the /// method if the property is - /// and the property + /// and the property /// is not . /// /// @@ -1616,12 +1616,12 @@ protected virtual void WriteDefaultValue(object defaultValue) /// information." or "Run 'executable command -Help' for more information." /// /// - /// If the property returns , + /// If the property returns , /// nothing is written. /// /// /// This method is called by the base implementation of the - /// method if the requested help is not . + /// method if the requested help is not . /// /// protected virtual void WriteMoreInfoMessage() @@ -1678,11 +1678,11 @@ protected virtual IEnumerable GetArgumentsInDescriptionOrde #region Subcommand usage /// - /// Creates the usage help for a instance. + /// Creates the usage help for a instance. /// /// /// - /// This is the primary method used to generate usage help for the + /// 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. diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs index 14da848a..dfe8bf8b 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs @@ -14,8 +14,8 @@ namespace Ookii.CommandLine.Validation; /// /// If validation fails, it will throw a with /// the category specified in the property. The -/// method, the -/// method and the +/// method, the +/// method and the /// class will automatically display the error message and usage /// help if validation failed. /// @@ -33,7 +33,7 @@ public abstract class ArgumentValidationAttribute : Attribute ///
/// /// One of the values of the enumeration. If not overridden - /// in a derived class, the value is . + /// in a derived class, the value is . /// public virtual ValidationMode Mode => ValidationMode.AfterConversion; @@ -43,7 +43,7 @@ public abstract class ArgumentValidationAttribute : Attribute ///
/// /// One of the values of the enumeration. If not overridden - /// in a derived class, the value is . + /// in a derived class, the value is . /// public virtual CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.ValidationFailed; @@ -53,10 +53,10 @@ public abstract class ArgumentValidationAttribute : Attribute /// The argument being validated. /// /// The argument value. If not , this must be an instance of - /// . + /// . /// /// - /// The parameter is not a valid value. The + /// The parameter is not a valid value. The /// property will be the value of the property. /// public void Validate(CommandLineArgument argument, object? value) @@ -78,7 +78,7 @@ public void Validate(CommandLineArgument argument, object? value) /// The argument being validated. /// /// The argument value. If not , this must be an instance of - /// . + /// . /// /// /// if validation was performed and successful; @@ -88,11 +88,11 @@ public void Validate(CommandLineArgument argument, object? value) /// /// /// The class will only call this method if the - /// property is . + /// property is . /// /// /// - /// The parameter is not a valid value. The + /// The parameter is not a valid value. The /// property will be the value of the property. /// public bool ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) @@ -118,31 +118,31 @@ public bool ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) /// The argument being validated. /// /// The argument value. If not , this must be a string or an - /// instance of . + /// instance of . /// /// /// if the value is valid; otherwise, . /// /// /// - /// If the property is , + /// If the property is , /// the parameter will be the raw string value provided by the /// user on the command line. /// /// - /// If the property is , + /// If the property is , /// for regular arguments, the parameter will be identical to - /// the property. For multi-value or dictionary + /// 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 , + /// If the property is , /// will always be . Use the - /// property instead. + /// property instead. /// /// - /// If you need to check the type of the argument, use the + /// 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. /// @@ -164,10 +164,10 @@ public bool ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) /// /// /// The class will only call this method if the - /// property is . + /// property is . /// /// - /// If you need to check the type of the argument, use the + /// 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. /// @@ -183,7 +183,7 @@ public bool ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) /// The argument that was validated. /// /// The argument value. If not , this must be an instance of - /// . + /// . /// /// The error message. /// @@ -205,7 +205,7 @@ public virtual string GetErrorMessage(CommandLineArgument argument, object? valu /// /// /// - /// This function is only called if the + /// This function is only called if the /// property is . /// /// diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs index 455f5239..2754f10c 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs @@ -7,7 +7,7 @@ /// /// 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. +/// directly and override the method. /// This class just adds some common functionality to make it easier. /// /// @@ -23,7 +23,7 @@ public abstract class ArgumentValidationWithHelpAttribute : ArgumentValidationAt /// /// /// - /// This has no effect if the + /// This has no effect if the /// property is . /// /// @@ -42,7 +42,7 @@ public abstract class ArgumentValidationWithHelpAttribute : ArgumentValidationAt /// /// /// - /// This function is only called if the + /// This function is only called if the /// property is . /// /// diff --git a/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs index f5db0d48..d57ab73a 100644 --- a/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs @@ -16,8 +16,8 @@ namespace Ookii.CommandLine.Validation; /// /// If validation fails, it will throw a with /// the category specified in the property. The -/// method, the -/// method and the +/// method, the +/// method and the /// class will automatically display the error message and usage /// help if validation failed. /// @@ -36,7 +36,7 @@ public abstract class ClassValidationAttribute : Attribute ///
/// /// One of the values of the enumeration. If not overridden - /// in a derived class, the value is . + /// in a derived class, the value is . /// public virtual CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.ValidationFailed; @@ -46,7 +46,7 @@ public abstract class ClassValidationAttribute : Attribute /// The argument parser being validated. /// /// The combination of arguments in the is not valid. The - /// property will be the value of the + /// property will be the value of the /// property. /// public void Validate(CommandLineParser parser) @@ -95,7 +95,7 @@ public virtual string GetErrorMessage(CommandLineParser parser) /// /// /// - /// This function is only called if the + /// This function is only called if the /// property is . /// /// diff --git a/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs b/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs index 71894bcd..fa6d7fac 100644 --- a/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs @@ -62,7 +62,7 @@ public DependencyValidationAttribute(bool requires, params string[] arguments) /// Gets a value that indicates when validation will run. ///
/// - /// . + /// . /// public override ValidationMode Mode => ValidationMode.AfterParsing; @@ -71,7 +71,7 @@ public DependencyValidationAttribute(bool requires, params string[] arguments) /// validation fails. ///
/// - /// . + /// . /// public override CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.DependencyFailed; diff --git a/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs b/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs index 9fb26097..55af06b5 100644 --- a/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs @@ -16,7 +16,7 @@ namespace Ookii.CommandLine.Validation; /// /// /// If validation fails, a is thrown with the -/// error category set to . +/// error category set to . /// /// /// Names of arguments that are dependencies are not validated when the attribute is created. diff --git a/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs b/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs index 791e8024..fcd8e8a6 100644 --- a/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs @@ -63,7 +63,7 @@ public class RequiresAnyAttribute : ClassValidationAttribute /// or is . /// /// - /// This constructor exists because + /// This constructor exists because /// is not CLS-compliant. /// public RequiresAnyAttribute(string argument1, string argument2) @@ -120,7 +120,7 @@ public RequiresAnyAttribute(params string[] arguments) /// validation fails. ///
/// - /// . + /// . /// public override CommandLineArgumentErrorCategory ErrorCategory => CommandLineArgumentErrorCategory.MissingRequiredArgument; @@ -135,7 +135,7 @@ public override CommandLineArgumentErrorCategory ErrorCategory /// /// /// - /// This has no effect if the + /// This has no effect if the /// property is . /// /// diff --git a/src/Ookii.CommandLine/Validation/RequiresAttribute.cs b/src/Ookii.CommandLine/Validation/RequiresAttribute.cs index 2c6fc0c1..63574321 100644 --- a/src/Ookii.CommandLine/Validation/RequiresAttribute.cs +++ b/src/Ookii.CommandLine/Validation/RequiresAttribute.cs @@ -16,7 +16,7 @@ namespace Ookii.CommandLine.Validation; /// /// /// If validation fails, a is thrown with the -/// error category set to . +/// error category set to . /// /// /// The names of the arguments that are dependencies are not validated when the attribute is diff --git a/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs index 3e416faa..8bdd41a3 100644 --- a/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs @@ -41,7 +41,7 @@ public ValidateCountAttribute(int minimum, int maximum = int.MaxValue) /// Gets a value that indicates when validation will run. ///
/// - /// . + /// . /// public override ValidationMode Mode => ValidationMode.AfterParsing; @@ -67,7 +67,7 @@ public ValidateCountAttribute(int minimum, int maximum = int.MaxValue) /// The argument being validated. /// /// The argument value. If not , this must be an instance of - /// . + /// . /// /// /// if the value is valid; otherwise, . diff --git a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs index 8534bdb8..bd600deb 100644 --- a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs @@ -22,12 +22,12 @@ namespace Ookii.CommandLine.Validation; /// /// /// This validator makes sure that the result of conversion is a valid value for the -/// enumeration, by using the method. +/// 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 +/// property to /// . Similarly, you can avoid listing all the values in the error /// message by setting the property to /// . diff --git a/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs index 1e2ece37..b7154001 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs @@ -23,7 +23,7 @@ public class ValidateNotEmptyAttribute : ArgumentValidationWithHelpAttribute /// Gets a value that indicates when validation will run. ///
/// - /// . + /// . /// public override ValidationMode Mode => ValidationMode.BeforeConversion; diff --git a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs index 4561825b..a529a356 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs @@ -38,7 +38,7 @@ public class ValidateNotNullAttribute : ArgumentValidationAttribute /// The argument being validated. /// /// The argument value. If not , this must be an instance of - /// . + /// . /// /// /// if the value is valid; otherwise, . diff --git a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs index 228a5e5d..457a3a54 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs @@ -25,7 +25,7 @@ public class ValidateNotWhiteSpaceAttribute : ArgumentValidationWithHelpAttribut /// Gets a value that indicates when validation will run. ///
/// - /// . + /// . /// public override ValidationMode Mode => ValidationMode.BeforeConversion; diff --git a/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs b/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs index 981a4e53..f283e403 100644 --- a/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs @@ -45,7 +45,7 @@ public ValidatePatternAttribute(string pattern, RegexOptions options = RegexOpti /// Gets a value that indicates when validation will run. ///
/// - /// . + /// . /// public override ValidationMode Mode => ValidationMode.BeforeConversion; @@ -60,7 +60,7 @@ public ValidatePatternAttribute(string pattern, RegexOptions options = RegexOpti /// /// /// If this property is , the message returned by - /// will be used. + /// will be used. /// /// /// This property is a compound format string, and may have three placeholders: diff --git a/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs index 73759bde..714bb0c0 100644 --- a/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs @@ -71,7 +71,7 @@ public ValidateRangeAttribute(object? minimum, object? maximum) /// The argument being validated. /// /// The argument value. If not , this must be an instance of - /// . + /// . /// /// /// if the value is valid; otherwise, . diff --git a/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs index b2b32dde..acc738ee 100644 --- a/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs @@ -32,7 +32,7 @@ public ValidateStringLengthAttribute(int minimum, int maximum = int.MaxValue) /// Gets a value that indicates when validation will run. ///
/// - /// . + /// . /// public override ValidationMode Mode => ValidationMode.BeforeConversion; diff --git a/src/Ookii.CommandLine/Validation/ValidationMode.cs b/src/Ookii.CommandLine/Validation/ValidationMode.cs index 4e121000..328a08be 100644 --- a/src/Ookii.CommandLine/Validation/ValidationMode.cs +++ b/src/Ookii.CommandLine/Validation/ValidationMode.cs @@ -8,20 +8,20 @@ public enum ValidationMode { /// /// Validation will occur after the value is converted. The value passed to - /// the method is an instance of the + /// 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. + /// 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 . + /// is always . /// AfterParsing, } diff --git a/src/Ookii.CommandLine/ValueDescriptionAttribute.cs b/src/Ookii.CommandLine/ValueDescriptionAttribute.cs index a1a17dac..5d99a2b8 100644 --- a/src/Ookii.CommandLine/ValueDescriptionAttribute.cs +++ b/src/Ookii.CommandLine/ValueDescriptionAttribute.cs @@ -9,7 +9,7 @@ namespace Ookii.CommandLine; /// /// The description of the value, or to indicate that the property's /// type name should be used, applying the specified by the -/// or +/// or /// property. /// /// @@ -18,16 +18,16 @@ namespace Ookii.CommandLine; /// type of value that the user should supply. ///
/// -/// If this attribute is not present, it is retrieved from the +/// 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 +/// cref="NameTransform"/> 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. +/// use the property. /// /// /// The value description is used only when generating usage help. For example, the usage for an @@ -42,7 +42,7 @@ namespace Ookii.CommandLine; /// using the attribute. /// ///
-/// +/// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] public class ValueDescriptionAttribute : Attribute { diff --git a/src/Ookii.CommandLine/WrappingMode.cs b/src/Ookii.CommandLine/WrappingMode.cs index 9c42e0bf..41aeb907 100644 --- a/src/Ookii.CommandLine/WrappingMode.cs +++ b/src/Ookii.CommandLine/WrappingMode.cs @@ -4,7 +4,7 @@ namespace Ookii.CommandLine; /// Indicates how the class will wrap text at the maximum /// line length. ///
-/// +/// public enum WrappingMode { /// From fa3d402930fa717684cb94527ce1789f6422e960 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 21 Jun 2023 11:42:47 -0700 Subject: [PATCH 179/234] Add Parse(ReadOnlyMemory) overload. --- src/Ookii.CommandLine/CommandLineParser.cs | 49 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index ca15e2fa..bab017de 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -271,6 +271,10 @@ private struct PrefixInfo /// be parsed. /// /// + /// + /// Instead of this constructor, it's recommended to use the + /// class instead. + /// /// /// This constructor uses reflection to determine the arguments defined by the type indicated /// by at runtime, unless the type has the @@ -1093,8 +1097,8 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = public static T? Parse(ParseOptions? options = null) where T : class { - // GetCommandLineArgs include the executable, so skip it. - return Parse(Environment.GetCommandLineArgs(), 1, options); + var parser = new CommandLineParser(options); + return parser.ParseWithErrorHandling(); } /// @@ -1129,11 +1133,46 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = public static T? Parse(string[] args, int index, ParseOptions? options = null) where T : class { - options ??= new(); - var parser = new CommandLineParser(typeof(T), options); - return (T?)parser.ParseWithErrorHandling(args, index); + var parser = new CommandLineParser(options); + return parser.ParseWithErrorHandling(args, index); + } + + /// + /// 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 options that control parsing behavior and usage help formatting. If + /// , the default options are used. + /// + /// + /// + /// + /// + /// is . + /// + /// + /// does not fall within the bounds of . + /// + /// + /// + /// + /// + /// + /// +#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(ReadOnlyMemory args, ParseOptions? options = null) + where T : class + { + var parser = new CommandLineParser(options); + return parser.ParseWithErrorHandling(args); } + /// /// Parses the specified command line arguments using the type . /// From e89f82b1e0ffe78f2c914d226a4cb0f90609276f Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 22 Jun 2023 10:32:14 -0700 Subject: [PATCH 180/234] More XML comment updates. --- docs/Migrating.md | 14 ++- docs/refs.json | 2 + .../AllowDuplicateDictionaryKeysAttribute.cs | 2 +- src/Ookii.CommandLine/CommandLineArgument.cs | 113 ++++++++++-------- .../CommandLineArgumentAttribute.cs | 48 ++++---- src/Ookii.CommandLine/CommandLineParser.cs | 63 +++++----- .../CommandLineParserGeneric.cs | 31 +++-- 7 files changed, 156 insertions(+), 117 deletions(-) diff --git a/docs/Migrating.md b/docs/Migrating.md index 0714d28a..0ef91baa 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -24,8 +24,8 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ - If you have existing conversions that depend on a [`TypeConverter`][], use the [`TypeConverterArgumentConverter`][] 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` + [`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 @@ -45,12 +45,16 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ - 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 `CommandLineArgumentAttribute.AllowsDuplicateDictionaryKeys` property was renamed to + [`AllowDuplicateDictionaryKeys`][] for consistency. ## Breaking behavior changes from version 3.0 @@ -116,8 +120,10 @@ 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. +[`AllowDuplicateDictionaryKeys`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgument_AllowDuplicateDictionaryKeys.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 @@ -136,6 +142,7 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`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 +[`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 @@ -149,6 +156,7 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`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-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 @@ -160,8 +168,8 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`TypeConverterArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter_1.htm [`TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute [`UsageWriter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_UsageWriter.htm -[`ValueDescriptionAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ValueDescriptionAttribute.htm [`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 [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 diff --git a/docs/refs.json b/docs/refs.json index b869158a..1fb9729b 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -4,6 +4,7 @@ "#suffix": ".htm", "AddCommand": null, "AliasAttribute": "T_Ookii_CommandLine_AliasAttribute", + "AllowDuplicateDictionaryKeys": "P_Ookii_CommandLine_CommandLineArgument_AllowDuplicateDictionaryKeys", "AllowDuplicateDictionaryKeysAttribute": "T_Ookii_CommandLine_AllowDuplicateDictionaryKeysAttribute", "ApplicationFriendlyNameAttribute": "T_Ookii_CommandLine_ApplicationFriendlyNameAttribute", "Arg1": null, @@ -234,6 +235,7 @@ "M_Ookii_CommandLine_LocalizedStringProvider_NullArgumentValue" ], "Ookii.CommandLine.Commands": "N_Ookii_CommandLine_Commands", + "Ookii.CommandLine.Conversion": "N_Ookii_CommandLine_Conversion", "Ookii.CommandLine.Conversion.Generated": null, "Ookii.CommandLine.Terminal": "N_Ookii_CommandLine_Terminal", "Ookii.CommandLine.Validation": "N_Ookii_CommandLine_Validation", diff --git a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs index c890a15f..03a9b35f 100644 --- a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs +++ b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs @@ -20,7 +20,7 @@ namespace Ookii.CommandLine; /// /// /// -/// +/// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] public sealed class AllowDuplicateDictionaryKeysAttribute : Attribute { diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 578f0333..f57246d2 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -14,7 +15,8 @@ namespace Ookii.CommandLine; /// -/// Provides information about command line arguments that are recognized by a . +/// Provides information about command line arguments that are recognized by an instance of the +/// class. /// /// /// @@ -561,13 +563,13 @@ public string? ShortNameWithPrefix /// 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. + /// 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 collection. + /// will always return an empty array. /// /// /// @@ -577,13 +579,14 @@ public string? ShortNameWithPrefix /// 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. + /// 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 collection. + /// will always return an empty array. /// /// /// @@ -629,8 +632,8 @@ public Type ArgumentType /// Gets the type of the values of a dictionary argument. ///
/// - /// The type of the values in the dictionary, or if - /// is . + /// The type of the values in the dictionary, or if the + /// property is . /// public Type? ValueType => _valueType; @@ -757,20 +760,21 @@ public string Description /// /// /// - /// 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 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 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>". + /// 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. + /// This is not the long description used to describe the purpose of the argument. That can be + /// retrieved using the property. /// /// /// @@ -809,12 +813,12 @@ public string Description /// the generic interface. /// /// - /// An argument is dictionary argument is a + /// 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 - /// property. + /// interface. /// /// /// An argument is if it is backed by a method instead @@ -825,7 +829,6 @@ public string Description /// Otherwise, the value will be . /// /// - /// public ArgumentKind Kind => _argumentKind; /// @@ -901,6 +904,9 @@ public string? MultiValueSeparator /// if this the property is ; /// otherwise, . /// +#if NET6_0_OR_GREATER + [MemberNotNullWhen(true, nameof(KeyType), nameof(ValueType), nameof(KeyValueSeparator)), CLSCompliant(false)] +#endif public bool IsDictionary => _argumentKind == ArgumentKind.Dictionary; /// @@ -915,10 +921,7 @@ public string? MultiValueSeparator /// /// /// - public bool AllowsDuplicateDictionaryKeys - { - get { return _allowDuplicateDictionaryKeys; } - } + public bool AllowDuplicateDictionaryKeys => _allowDuplicateDictionaryKeys; /// /// Gets the value that the argument was set to in the last call to . @@ -981,15 +984,20 @@ public bool AllowsDuplicateDictionaryKeys /// 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. + /// 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. + /// 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. + /// 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; @@ -998,9 +1006,10 @@ public bool AllowsDuplicateDictionaryKeys /// 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. + /// 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. /// /// /// @@ -1011,14 +1020,13 @@ public bool AllowsDuplicateDictionaryKeys /// 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. + /// 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 used for this argument returns - /// from its - /// method. + /// 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 . @@ -1026,11 +1034,13 @@ public bool AllowsDuplicateDictionaryKeys /// 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 + /// 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. /// /// @@ -1098,11 +1108,11 @@ public bool AllowsDuplicateDictionaryKeys /// /// /// Conversion is done by one of several methods. First, if a was present on the property, or method that + /// cref="ArgumentConverterAttribute"/> 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 + /// , or have a static Parse(, + /// ) or Parse() method, or have a /// constructor that takes a single parameter of type . /// /// @@ -1121,17 +1131,13 @@ public bool AllowsDuplicateDictionaryKeys /// /// 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 . Other types cannot be - /// converted. + /// method, using . Other types + /// will be converted to a string before conversion. /// /// /// This method is used to convert the @@ -1139,7 +1145,10 @@ public bool AllowsDuplicateDictionaryKeys /// class to convert values when needed. /// /// - /// The conversion is not supported. + /// + /// The argument's cannot convert between the type of + /// and the . + /// public object? ConvertToArgumentTypeInvariant(object? value) { if (value == null || _elementTypeWithNullable.IsAssignableFrom(value.GetType())) diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index 723f1253..8c8eb681 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -42,7 +42,7 @@ namespace Ookii.CommandLine; /// . /// /// -/// Unlike using the property event, canceling parsing with the return +/// 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 @@ -143,13 +143,17 @@ public string? ArgumentName /// /// /// - /// This property is ignored if is not - /// . + /// This property is ignored if the + /// property 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. /// + /// + /// If the property is set to a value other than the null character, + /// this property will always return . + /// /// /// public bool IsShort @@ -164,8 +168,8 @@ public bool IsShort /// The short name, or a null character ('\0') if the argument has no short name. /// /// - /// This property is ignored if is not - /// . + /// This property is ignored if the + /// property is not . /// /// /// Setting this property implies the property is . @@ -176,7 +180,7 @@ public bool IsShort /// property. /// /// - /// + /// public char ShortName { get; set; } /// @@ -186,11 +190,18 @@ public bool IsShort /// 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 the position of a positional argument. + /// 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. @@ -206,7 +217,7 @@ public bool IsShort /// /// /// When using the , you can also set the - /// property to without setting the property + /// property to , without setting the property, /// to order the positional arguments using the order of the members that define them. /// /// @@ -215,7 +226,7 @@ public bool IsShort /// /// /// The property will be set to reflect the actual position of the argument, - /// which may not match the value of the property. + /// which may not match the value of this property. /// /// /// @@ -239,8 +250,8 @@ public bool IsShort /// members that define them. /// /// - /// Doing this is not supported without the , because - /// reflection is not guaranteed to return class members in any particular order. The + /// 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. @@ -294,8 +305,8 @@ public bool IsPositional /// /// /// The default value can be set using the property, or, when - /// using source generation with the , using a property - /// initializer. + /// using source generation with the attribute, using a + /// property initializer. /// /// /// This property is ignored if the @@ -330,13 +341,8 @@ public bool IsPositional /// cancellation. /// /// - /// The method and the - /// static helper method - /// will print usage information if parsing was canceled with . - /// - /// - /// Canceling parsing in this way is identical to handling the - /// event and setting property. + /// 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 diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index bab017de..ed9b7676 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -200,8 +200,8 @@ private struct PrefixInfo private List? _requiredPropertyArguments; /// - /// Gets the default prefix used for long argument names if is - /// . + /// Gets the default prefix used for long argument names if the + /// property is . /// /// /// The default long argument name prefix, which is '--'. @@ -209,6 +209,8 @@ private struct PrefixInfo /// /// /// This constant is used as the default value of the + /// property if no custom value was specified using the + /// property of the /// property. /// /// @@ -219,24 +221,28 @@ private struct PrefixInfo /// /// /// - /// 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. + /// 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. /// /// - /// 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 . + /// 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. /// /// - /// This event is invoked after the and properties have been set. + /// 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. + /// Event raised when an argument that is not multi-value is specified more than once. /// /// /// @@ -798,7 +804,7 @@ public static string GetExecutableName(bool includeExtension = false) } /// - /// Writes command line usage help to the specified using the specified options. + /// Writes command line usage help using the specified instance. /// /// /// The to use to create the usage. If , @@ -935,14 +941,13 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// 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 . + /// that returned or . /// /// /// - /// 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 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 @@ -955,8 +960,8 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// public object? ParseWithErrorHandling() { - // GetCommandLineArgs include the executable, so skip it. - return ParseWithErrorHandling(Environment.GetCommandLineArgs(), 1); + // GetCommandLineArgs includes the executable, so skip it. + return ParseWithErrorHandling(Environment.GetCommandLineArgs().AsMemory(1)); } /// @@ -1059,11 +1064,12 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// - /// 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 . + /// 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 @@ -1150,12 +1156,6 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// /// - /// - /// is . - /// - /// - /// does not fall within the bounds of . - /// /// /// /// @@ -1203,7 +1203,8 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = public static T? Parse(string[] args, ParseOptions? options = null) where T : class { - return Parse(args, 0, options); + var parser = new CommandLineParser(options); + return parser.ParseWithErrorHandling(args); } /// diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index 52dd3881..b20ccd2b 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -7,21 +7,18 @@ namespace Ookii.CommandLine; /// /// A generic version of the class that offers strongly typed -/// methods. +/// and methods. /// /// The type that defines the arguments. /// /// /// 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. -/// -/// -/// 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. +/// 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 { @@ -38,7 +35,23 @@ public class CommandLineParser : CommandLineParser /// names or positions, or has an argument type that cannot be parsed. /// /// - /// + /// + /// 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. + /// + /// + /// 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)] From bc3a4fc36599d92e64f05b088bdabbc25ca496e6 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 22 Jun 2023 11:15:30 -0700 Subject: [PATCH 181/234] Refactor dictionary argument related info. --- .../CommandLineParserTest.cs | 12 +-- .../AllowDuplicateDictionaryKeysAttribute.cs | 2 +- src/Ookii.CommandLine/CommandLineArgument.cs | 88 +++---------------- .../DictionaryArgumentInfo.cs | 68 ++++++++++++++ .../Support/GeneratedArgument.cs | 13 ++- .../Support/ReflectionArgument.cs | 21 ++--- 6 files changed, 104 insertions(+), 100 deletions(-) create mode 100644 src/Ookii.CommandLine/DictionaryArgumentInfo.cs diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 58ffdf8a..deed7038 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -261,9 +261,9 @@ public void ParseTestNameValueSeparator(ProviderKind kind) public void ParseTestKeyValueSeparator(ProviderKind kind) { var target = CreateParser(kind); - Assert.AreEqual("=", target.GetArgument("DefaultSeparator")!.KeyValueSeparator); + Assert.AreEqual("=", target.GetArgument("DefaultSeparator")!.DictionaryInfo!.KeyValueSeparator); Assert.AreEqual("String=Int32", target.GetArgument("DefaultSeparator")!.ValueDescription); - Assert.AreEqual("<=>", target.GetArgument("CustomSeparator")!.KeyValueSeparator); + 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<=>" }); @@ -1141,7 +1141,7 @@ public void TestDuplicateArguments(ProviderKind kind) bool handlerCalled = false; bool keepOldValue = false; - EventHandler handler = (sender, e) => + void handler(object? sender, DuplicateArgumentEventArgs e) { Assert.AreEqual("Argument1", e.Argument.ArgumentName); Assert.AreEqual("foo", e.Argument.Value); @@ -1151,7 +1151,7 @@ public void TestDuplicateArguments(ProviderKind kind) { e.KeepOldValue = true; } - }; + } parser.DuplicateArgument += handler; @@ -1348,8 +1348,8 @@ private static void VerifyArgument(CommandLineArgument? argument, ExpectedArgume 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.Kind is ArgumentKind.MultiValue or ArgumentKind.Dictionary, argument.IsMultiValue); + 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); diff --git a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs index 03a9b35f..854a035f 100644 --- a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs +++ b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs @@ -20,7 +20,7 @@ namespace Ookii.CommandLine; /// /// /// -/// +/// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] public sealed class AllowDuplicateDictionaryKeysAttribute : Attribute { diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index f57246d2..152c9170 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -296,14 +296,11 @@ internal struct ArgumentInfo 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 CancelMode CancelParsing { get; set; } public bool IsHidden { get; set; } - public Type? KeyType { get; set; } - public Type? ValueType { get; set; } public IEnumerable Validators { get; set; } + public DictionaryArgumentInfo? DictionaryInfo { get; set; } } #endregion @@ -318,17 +315,13 @@ internal struct ArgumentInfo private readonly Type _argumentType; private readonly Type _elementType; private readonly Type _elementTypeWithNullable; - private readonly Type? _keyType; - private readonly Type? _valueType; 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 CancelMode _cancelParsing; private readonly bool _isHidden; @@ -383,8 +376,6 @@ internal CommandLineArgument(ArgumentInfo info) _argumentKind = info.Kind; _elementTypeWithNullable = info.ElementTypeWithNullable; _elementType = info.ElementType; - _keyType = info.KeyType; - _valueType = info.ValueType; _description = info.Description; _isRequired = info.IsRequired; IsRequiredProperty = info.IsRequiredProperty; @@ -398,11 +389,10 @@ internal CommandLineArgument(ArgumentInfo info) _defaultValue = ConvertToArgumentTypeInvariant(info.DefaultValue); IncludeDefaultInUsageHelp = info.IncludeDefaultValueInHelp; _valueDescription = info.ValueDescription; - _allowDuplicateDictionaryKeys = info.AllowDuplicateDictionaryKeys; _allowMultiValueWhiteSpaceSeparator = IsMultiValue && !IsSwitch && info.AllowMultiValueWhiteSpaceSeparator; _allowNull = info.AllowNull; - _keyValueSeparator = info.KeyValueSeparator; _multiValueSeparator = info.MultiValueSeparator; + DictionaryInfo = info.DictionaryInfo; } /// @@ -619,24 +609,6 @@ public Type ArgumentType /// public Type ElementType => _elementType; - /// - /// Gets the type of the keys of a dictionary argument. - /// - /// - /// The type of the keys in the dictionary, or if - /// is . - /// - public Type? KeyType => _keyType; - - /// - /// Gets the type of the values of a dictionary argument. - /// - /// - /// The type of the values in the dictionary, or if the - /// property is . - /// - public Type? ValueType => _valueType; - /// /// Gets the position of this argument. /// @@ -883,45 +855,13 @@ public string? MultiValueSeparator 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, . - /// -#if NET6_0_OR_GREATER - [MemberNotNullWhen(true, nameof(KeyType), nameof(ValueType), nameof(KeyValueSeparator)), CLSCompliant(false)] -#endif - public bool IsDictionary => _argumentKind == ArgumentKind.Dictionary; - - /// - /// Gets a value indicating whether this argument, if it is a dictionary argument, allows duplicate keys. + /// Gets information that only applies to dictionary arguments. /// /// - /// if this argument allows duplicate keys; otherwise, . + /// An instance of the class, or + /// if /// - /// - /// - /// This property is only meaningful if the property is . - /// - /// - /// - public bool AllowDuplicateDictionaryKeys => _allowDuplicateDictionaryKeys; + public DictionaryArgumentInfo? DictionaryInfo { get; } /// /// Gets the value that the argument was set to in the last call to . @@ -1228,8 +1168,6 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, MultiValueSeparatorAttribute? multiValueSeparatorAttribute, DescriptionAttribute? descriptionAttribute, ValueDescriptionAttribute? valueDescriptionAttribute, - bool allowDuplicateDictionaryKeys, - KeyValueSeparatorAttribute? keyValueSeparatorAttribute, IEnumerable? aliasAttributes, IEnumerable? shortAliasAttributes, IEnumerable? validationAttributes) @@ -1247,10 +1185,8 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, Description = descriptionAttribute?.Description, ValueDescription = valueDescriptionAttribute?.ValueDescription, Position = attribute.Position < 0 ? null : attribute.Position, - AllowDuplicateDictionaryKeys = allowDuplicateDictionaryKeys, MultiValueSeparator = GetMultiValueSeparator(multiValueSeparatorAttribute), AllowMultiValueWhiteSpaceSeparator = multiValueSeparatorAttribute != null && multiValueSeparatorAttribute.Separator == null, - KeyValueSeparator = keyValueSeparatorAttribute?.Separator, Aliases = GetAliases(aliasAttributes, argumentName), ShortAliases = GetShortAliases(shortAliasAttributes, argumentName), DefaultValue = attribute.DefaultValue, @@ -1273,11 +1209,11 @@ private string DetermineValueDescription(Type? type = null) return result; } - if (Kind == ArgumentKind.Dictionary && type == null) + if (type == null && DictionaryInfo != null) { - var key = DetermineValueDescription(_keyType!.GetUnderlyingType()); - var value = DetermineValueDescription(_valueType!.GetUnderlyingType()); - return $"{key}{KeyValueSeparator}{value}"; + var key = DetermineValueDescription(DictionaryInfo.KeyType.GetUnderlyingType()); + var value = DetermineValueDescription(DictionaryInfo.ValueType.GetUnderlyingType()); + return $"{key}{DictionaryInfo.KeyValueSeparator}{value}"; } var typeName = DetermineValueDescriptionForType(type ?? ElementType); @@ -1342,7 +1278,7 @@ private static string GetFriendlyTypeName(Type type) ? _converter.Convert(spanValue, culture, this) : _converter.Convert(stringValue, culture, this); - if (converted == null && (!_allowNull || IsDictionary)) + if (converted == null && (!_allowNull || Kind == ArgumentKind.Dictionary)) { throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.NullArgumentValue, this); } @@ -1548,7 +1484,7 @@ private IValueHelper CreateValueHelper() { case ArgumentKind.Dictionary: type = typeof(DictionaryValueHelper<,>).MakeGenericType(_elementType.GetGenericArguments()); - return (IValueHelper)Activator.CreateInstance(type, _allowDuplicateDictionaryKeys, _allowNull)!; + return (IValueHelper)Activator.CreateInstance(type, DictionaryInfo!.AllowDuplicateDictionaryKeys, _allowNull)!; case ArgumentKind.MultiValue: type = typeof(MultiValueHelper<>).MakeGenericType(_elementTypeWithNullable); diff --git a/src/Ookii.CommandLine/DictionaryArgumentInfo.cs b/src/Ookii.CommandLine/DictionaryArgumentInfo.cs new file mode 100644 index 00000000..dc110c31 --- /dev/null +++ b/src/Ookii.CommandLine/DictionaryArgumentInfo.cs @@ -0,0 +1,68 @@ +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 allowDuplicateDictionaryKeys, Type keyType, Type valueType, string keyValueSeparator) + { + AllowDuplicateDictionaryKeys = allowDuplicateDictionaryKeys; + 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, if it is a dictionary argument, allows duplicate keys. + /// + /// + /// if this argument allows duplicate keys; otherwise, . + /// + /// + public bool AllowDuplicateDictionaryKeys { 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 if this argument is a dictionary argument. + /// + /// + /// The custom value specified using the attribute, or + /// if no attribute was + /// present. + /// + /// + public string KeyValueSeparator { get; } +} diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 70bebb0d..4563f868 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -112,8 +112,8 @@ public static GeneratedArgument Create(CommandLineParser parser, } var info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, memberName, attribute, - multiValueSeparatorAttribute, descriptionAttribute, valueDescriptionAttribute, allowDuplicateDictionaryKeys, - keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); + multiValueSeparatorAttribute, descriptionAttribute, valueDescriptionAttribute, aliasAttributes, shortAliasAttributes, + validationAttributes); info.ElementType = elementType; info.ElementTypeWithNullable = elementTypeWithNullable; @@ -122,9 +122,8 @@ public static GeneratedArgument Create(CommandLineParser parser, info.DefaultValue ??= alternateDefaultValue; if (info.Kind == ArgumentKind.Dictionary) { - info.KeyValueSeparator ??= KeyValuePairConverter.DefaultSeparator; - info.KeyType = keyType; - info.ValueType = valueType; + info.DictionaryInfo = new(allowDuplicateDictionaryKeys, keyType!, valueType!, + keyValueSeparatorAttribute?.Separator ?? KeyValuePairConverter.DefaultSeparator); } return new GeneratedArgument(info, setProperty, getProperty, callMethod, defaultValueDescription, defaultKeyDescription); @@ -169,8 +168,8 @@ protected override void SetProperty(object target, object? value) /// protected override string DetermineValueDescriptionForType(Type type) { - Debug.Assert(type == KeyType || type == ValueType || (ValueType == null && type == ElementType)); - if (KeyType != null && type == KeyType) + Debug.Assert(DictionaryInfo == null ? type == ElementType : (type == DictionaryInfo.KeyType || type == DictionaryInfo.ValueType)); + if (DictionaryInfo != null && type == DictionaryInfo.KeyType) { return _defaultKeyDescription!; } diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index 1dc5ce06..68ffe0b7 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -148,14 +148,15 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo #endif ArgumentInfo info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, member.Name, attribute, - multiValueSeparatorAttribute, descriptionAttribute, valueDescriptionAttribute, allowDuplicateDictionaryKeys, - keyValueSeparatorAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); + multiValueSeparatorAttribute, descriptionAttribute, valueDescriptionAttribute, aliasAttributes, shortAliasAttributes, + validationAttributes); - DetermineAdditionalInfo(ref info, member); + DetermineAdditionalInfo(ref info, member, keyValueSeparatorAttribute, allowDuplicateDictionaryKeys); return new ReflectionArgument(info, property, method); } - private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo member) + private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo member, + KeyValueSeparatorAttribute? keyValueSeparatorAttribute, bool allowDuplicateDictionaryKeys) { var converterAttribute = member.GetCustomAttribute(); var keyArgumentConverterAttribute = member.GetCustomAttribute(); @@ -173,17 +174,17 @@ private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo me info.Kind = ArgumentKind.Dictionary; info.ElementTypeWithNullable = elementType!; info.AllowNull = DetermineDictionaryValueTypeAllowsNull(dictionaryType, property); - info.KeyValueSeparator ??= KeyValuePairConverter.DefaultSeparator; var genericArguments = dictionaryType.GetGenericArguments(); - info.KeyType = genericArguments[0]; - info.ValueType = genericArguments[1]; + info.DictionaryInfo = new(allowDuplicateDictionaryKeys, genericArguments[0], genericArguments[1], + keyValueSeparatorAttribute?.Separator ?? KeyValuePairConverter.DefaultSeparator); + if (converterType == null) { converterType = typeof(KeyValuePairConverter<,>).MakeGenericType(genericArguments); - var keyConverter = info.KeyType.GetStringConverter(keyArgumentConverterAttribute?.GetConverterType()); - var valueConverter = info.ValueType.GetStringConverter(valueArgumentConverterAttribute?.GetConverterType()); + 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.KeyValueSeparator, info.AllowNull)!; + info.DictionaryInfo.KeyValueSeparator, info.AllowNull)!; } } else if (collectionType != null) From 8e00f94228f501d2c9eea63558998bc55ab5ce2a Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 22 Jun 2023 12:33:53 -0700 Subject: [PATCH 182/234] Refactor multi-value argument info. --- .../CommandLineParserTest.cs | 10 +- src/Ookii.CommandLine/CommandLineArgument.cs | 101 ++++++------------ src/Ookii.CommandLine/CommandLineParser.cs | 16 +-- .../DictionaryArgumentInfo.cs | 1 + .../MultiValueArgumentInfo.cs | 59 ++++++++++ .../Support/GeneratedArgument.cs | 13 ++- .../Support/ReflectionArgument.cs | 12 ++- src/Ookii.CommandLine/UsageWriter.cs | 2 +- .../Validation/ValidateCountAttribute.cs | 2 +- 9 files changed, 123 insertions(+), 93 deletions(-) create mode 100644 src/Ookii.CommandLine/MultiValueArgumentInfo.cs diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index deed7038..3fd317c3 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1100,9 +1100,9 @@ public void TestDefaultValueDescriptions(ProviderKind kind) public void TestMultiValueWhiteSpaceSeparator(ProviderKind kind) { var parser = CreateParser(kind); - Assert.IsTrue(parser.GetArgument("Multi")!.AllowMultiValueWhiteSpaceSeparator); - Assert.IsFalse(parser.GetArgument("MultiSwitch")!.AllowMultiValueWhiteSpaceSeparator); - Assert.IsFalse(parser.GetArgument("Other")!.AllowMultiValueWhiteSpaceSeparator); + 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); @@ -1348,12 +1348,12 @@ private static void VerifyArgument(CommandLineArgument? argument, ExpectedArgume 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.IsMultiValue); + 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.AllowMultiValueWhiteSpaceSeparator); + Assert.IsFalse(argument.MultiValueInfo?.AllowWhiteSpaceSeparator ?? false); Assert.IsNull(argument.Value); Assert.IsFalse(argument.HasValue); CollectionAssert.AreEqual(expected.Aliases ?? Array.Empty(), argument.Aliases); diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 152c9170..d4a4d983 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -294,12 +294,11 @@ internal struct ArgumentInfo public bool IncludeDefaultValueInHelp { get; set; } public string? Description { get; set; } public string? ValueDescription { get; set; } - public string? MultiValueSeparator { get; set; } - public bool AllowMultiValueWhiteSpaceSeparator { 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; } } @@ -320,8 +319,6 @@ internal struct ArgumentInfo private readonly string _memberName; private readonly object? _defaultValue; private readonly ArgumentKind _argumentKind; - private readonly string? _multiValueSeparator; - private readonly bool _allowMultiValueWhiteSpaceSeparator; private readonly bool _allowNull; private readonly CancelMode _cancelParsing; private readonly bool _isHidden; @@ -389,10 +386,13 @@ internal CommandLineArgument(ArgumentInfo info) _defaultValue = ConvertToArgumentTypeInvariant(info.DefaultValue); IncludeDefaultInUsageHelp = info.IncludeDefaultValueInHelp; _valueDescription = info.ValueDescription; - _allowMultiValueWhiteSpaceSeparator = IsMultiValue && !IsSwitch && info.AllowMultiValueWhiteSpaceSeparator; _allowNull = info.AllowNull; - _multiValueSeparator = info.MultiValueSeparator; DictionaryInfo = info.DictionaryInfo; + MultiValueInfo = info.MultiValueInfo; + if (MultiValueInfo != null && IsSwitch) + { + MultiValueInfo.AllowWhiteSpaceSeparator = false; + } } /// @@ -597,16 +597,12 @@ public Type ArgumentType /// 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 , + /// 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. /// - /// - /// - /// For a dictionary argument, the element type is . - /// - /// public Type ElementType => _elementType; /// @@ -804,63 +800,36 @@ public string Description public ArgumentKind Kind => _argumentKind; /// - /// Gets a value indicating whether this argument is a multi-value argument. + /// Gets information that only applies to multi-value or dictionary arguments. /// /// - /// 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. + /// An instance of the class, or + /// if the property is not + /// or . /// /// /// - /// If the property is , this property - /// is always . + /// 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 string? MultiValueSeparator - { - get { return _multiValueSeparator; } - } + public MultiValueArgumentInfo? MultiValueInfo { get; } /// - /// Gets a value that indicates whether or not a multi-value argument can consume multiple - /// following argument values. + /// Gets information that only applies to dictionary arguments. /// /// - /// if a multi-value argument can consume multiple following values; - /// otherwise, . + /// An instance of the class, or + /// if the property is not . /// /// /// - /// 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 . + /// Since dictionary arguments are a type of multi-value argument, also see the + /// property. /// /// - /// - public bool AllowMultiValueWhiteSpaceSeparator => _allowMultiValueWhiteSpaceSeparator; - - /// - /// Gets information that only applies to dictionary arguments. - /// - /// - /// An instance of the class, or - /// if - /// public DictionaryArgumentInfo? DictionaryInfo { get; } /// @@ -1165,7 +1134,6 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, bool requiredProperty, string memberName, CommandLineArgumentAttribute attribute, - MultiValueSeparatorAttribute? multiValueSeparatorAttribute, DescriptionAttribute? descriptionAttribute, ValueDescriptionAttribute? valueDescriptionAttribute, IEnumerable? aliasAttributes, @@ -1185,8 +1153,6 @@ internal static ArgumentInfo CreateArgumentInfo(CommandLineParser parser, Description = descriptionAttribute?.Description, ValueDescription = valueDescriptionAttribute?.ValueDescription, Position = attribute.Position < 0 ? null : attribute.Position, - MultiValueSeparator = GetMultiValueSeparator(multiValueSeparatorAttribute), - AllowMultiValueWhiteSpaceSeparator = multiValueSeparatorAttribute != null && multiValueSeparatorAttribute.Separator == null, Aliases = GetAliases(aliasAttributes, argumentName), ShortAliases = GetShortAliases(shortAliasAttributes, argumentName), DefaultValue = attribute.DefaultValue, @@ -1343,10 +1309,10 @@ internal CancelMode SetValue(CultureInfo culture, bool hasValue, string? stringV _valueHelper ??= CreateValueHelper(); CancelMode cancelParsing; - if (IsMultiValue && hasValue && MultiValueSeparator != null) + if (MultiValueInfo?.Separator != null) { cancelParsing = CancelMode.None; - spanValue.Split(MultiValueSeparator.AsSpan(), separateValue => + spanValue.Split(MultiValueInfo.Separator.AsSpan(), separateValue => { string? separateValueString = null; PreValidate(ref separateValueString, separateValue); @@ -1436,7 +1402,7 @@ internal void ApplyPropertyValue(object target) internal void Reset() { - if (!IsMultiValue && _defaultValue != null) + if (MultiValueInfo == null && _defaultValue != null) { _valueHelper = new SingleValueHelper(_defaultValue); } @@ -1597,16 +1563,13 @@ private void PreValidate(ref string? stringValue, ReadOnlySpan spanValue) } } } - private static string? GetMultiValueSeparator(MultiValueSeparatorAttribute? attribute) + + internal static MultiValueArgumentInfo GetMultiValueInfo(MultiValueSeparatorAttribute? attribute) { var separator = attribute?.Separator; - if (string.IsNullOrEmpty(separator)) - { - return null; - } - else - { - return separator; - } + return new( + string.IsNullOrEmpty(separator) ? null : separator, + attribute != null && separator == null + ); } } diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index ed9b7676..d437221b 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1499,13 +1499,13 @@ private void AddNamedArgument(CommandLineArgument argument, ImmutableArray? memoryValue) { - if (argument.HasValue && !argument.IsMultiValue) + if (argument.HasValue && argument.MultiValueInfo == null) { if (!AllowDuplicateArguments) { @@ -1734,7 +1734,7 @@ private CancelMode ParseArgumentValue(CommandLineArgument argument, string? stri return (cancelParsing, index, argument); } - if (!argument.AllowMultiValueWhiteSpaceSeparator) + if (argument.MultiValueInfo is not MultiValueArgumentInfo { AllowWhiteSpaceSeparator: true }) { break; } diff --git a/src/Ookii.CommandLine/DictionaryArgumentInfo.cs b/src/Ookii.CommandLine/DictionaryArgumentInfo.cs index dc110c31..34cdd19a 100644 --- a/src/Ookii.CommandLine/DictionaryArgumentInfo.cs +++ b/src/Ookii.CommandLine/DictionaryArgumentInfo.cs @@ -6,6 +6,7 @@ namespace Ookii.CommandLine; /// /// Provides information that only applies to dictionary arguments. /// +/// public sealed class DictionaryArgumentInfo { /// 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/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 4563f868..4dcb390d 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -112,18 +112,21 @@ public static GeneratedArgument Create(CommandLineParser parser, } var info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, memberName, attribute, - multiValueSeparatorAttribute, descriptionAttribute, valueDescriptionAttribute, aliasAttributes, shortAliasAttributes, - validationAttributes); + descriptionAttribute, valueDescriptionAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); info.ElementType = elementType; info.ElementTypeWithNullable = elementTypeWithNullable; info.Converter = converter; info.Kind = kind; info.DefaultValue ??= alternateDefaultValue; - if (info.Kind == ArgumentKind.Dictionary) + if (info.Kind is ArgumentKind.MultiValue or ArgumentKind.Dictionary) { - info.DictionaryInfo = new(allowDuplicateDictionaryKeys, keyType!, valueType!, - keyValueSeparatorAttribute?.Separator ?? KeyValuePairConverter.DefaultSeparator); + 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); diff --git a/src/Ookii.CommandLine/Support/ReflectionArgument.cs b/src/Ookii.CommandLine/Support/ReflectionArgument.cs index 68ffe0b7..f01b610b 100644 --- a/src/Ookii.CommandLine/Support/ReflectionArgument.cs +++ b/src/Ookii.CommandLine/Support/ReflectionArgument.cs @@ -148,15 +148,17 @@ private static CommandLineArgument Create(CommandLineParser parser, PropertyInfo #endif ArgumentInfo info = CreateArgumentInfo(parser, argumentType, allowsNull, requiredProperty, member.Name, attribute, - multiValueSeparatorAttribute, descriptionAttribute, valueDescriptionAttribute, aliasAttributes, shortAliasAttributes, - validationAttributes); + descriptionAttribute, valueDescriptionAttribute, aliasAttributes, shortAliasAttributes, validationAttributes); + + DetermineAdditionalInfo(ref info, member, multiValueSeparatorAttribute, keyValueSeparatorAttribute, + allowDuplicateDictionaryKeys); - DetermineAdditionalInfo(ref info, member, keyValueSeparatorAttribute, allowDuplicateDictionaryKeys); return new ReflectionArgument(info, property, method); } private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo member, - KeyValueSeparatorAttribute? keyValueSeparatorAttribute, bool allowDuplicateDictionaryKeys) + MultiValueSeparatorAttribute? multiValueSeparatorAttribute, KeyValueSeparatorAttribute? keyValueSeparatorAttribute, + bool allowDuplicateDictionaryKeys) { var converterAttribute = member.GetCustomAttribute(); var keyArgumentConverterAttribute = member.GetCustomAttribute(); @@ -172,6 +174,7 @@ private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo me { Debug.Assert(elementType != null); info.Kind = ArgumentKind.Dictionary; + info.MultiValueInfo = GetMultiValueInfo(multiValueSeparatorAttribute); info.ElementTypeWithNullable = elementType!; info.AllowNull = DetermineDictionaryValueTypeAllowsNull(dictionaryType, property); var genericArguments = dictionaryType.GetGenericArguments(); @@ -191,6 +194,7 @@ private static void DetermineAdditionalInfo(ref ArgumentInfo info, MemberInfo me { Debug.Assert(elementType != null); info.Kind = ArgumentKind.MultiValue; + info.MultiValueInfo = GetMultiValueInfo(multiValueSeparatorAttribute); info.ElementTypeWithNullable = elementType!; info.AllowNull = DetermineCollectionElementTypeAllowsNull(collectionType, property); } diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index d5dc1aaf..cef486ea 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -1068,7 +1068,7 @@ protected virtual void WriteArgumentSyntax(CommandLineArgument argument) WriteValueDescription(argument.ValueDescription); } - if (argument.IsMultiValue) + if (argument.MultiValueInfo != null) { WriteMultiValueSuffix(); } diff --git a/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs index 8bdd41a3..f0e39584 100644 --- a/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs @@ -74,7 +74,7 @@ public ValidateCountAttribute(int minimum, int maximum = int.MaxValue) /// public override bool IsValid(CommandLineArgument argument, object? value) { - if (!argument.IsMultiValue) + if (argument.MultiValueInfo == null) { return false; } From ce4f78024e49b926849d04489a0f1ce782518a0d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 22 Jun 2023 15:51:21 -0700 Subject: [PATCH 183/234] More XML comment updates. --- src/Ookii.CommandLine/AliasAttribute.cs | 25 ++-- .../AllowDuplicateDictionaryKeysAttribute.cs | 18 ++- .../ApplicationFriendlyNameAttribute.cs | 13 +- .../ArgumentParsedEventArgs.cs | 4 +- src/Ookii.CommandLine/CancelMode.cs | 2 +- src/Ookii.CommandLine/CommandLineArgument.cs | 2 +- .../CommandLineArgumentErrorCategory.cs | 9 +- .../CommandLineArgumentException.cs | 21 +-- src/Ookii.CommandLine/CommandLineParser.cs | 2 +- .../DescriptionListFilterMode.cs | 2 +- .../DescriptionListSortMode.cs | 6 +- .../DictionaryArgumentInfo.cs | 12 +- .../DuplicateArgumentEventArgs.cs | 10 +- .../GeneratedParserAttribute.cs | 39 +++-- src/Ookii.CommandLine/IParser.cs | 31 ++-- src/Ookii.CommandLine/IParserProvider.cs | 11 +- .../LineWrappingTextWriter.cs | 47 ++++-- .../LocalizedStringProvider.Error.cs | 8 +- .../LocalizedStringProvider.Validators.cs | 48 ++++--- .../LocalizedStringProvider.cs | 13 +- .../MultiValueSeparatorAttribute.cs | 3 +- src/Ookii.CommandLine/NameTransform.cs | 4 +- .../NameTransformExtensions.cs | 3 +- src/Ookii.CommandLine/ParseOptions.cs | 136 +++++++++++------- 24 files changed, 277 insertions(+), 192 deletions(-) diff --git a/src/Ookii.CommandLine/AliasAttribute.cs b/src/Ookii.CommandLine/AliasAttribute.cs index ee15bd04..59d7beb4 100644 --- a/src/Ookii.CommandLine/AliasAttribute.cs +++ b/src/Ookii.CommandLine/AliasAttribute.cs @@ -3,38 +3,41 @@ namespace Ookii.CommandLine; /// -/// Defines an alternative name for a command line argument or a subcommand. +/// 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"). +/// 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. +/// 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 +/// 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. +/// 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 +/// the command list usage help is controlled by the /// property. /// /// -/// +/// /// +/// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = true)] public sealed class AliasAttribute : Attribute { diff --git a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs index 854a035f..a6c8b895 100644 --- a/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs +++ b/src/Ookii.CommandLine/AllowDuplicateDictionaryKeysAttribute.cs @@ -8,19 +8,23 @@ namespace Ookii.CommandLine; /// /// /// -/// 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 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 -/// of will be thrown when a duplicate key is specified. +/// 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. +/// 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 ee38c521..af60d029 100644 --- a/src/Ookii.CommandLine/ApplicationFriendlyNameAttribute.cs +++ b/src/Ookii.CommandLine/ApplicationFriendlyNameAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; namespace Ookii.CommandLine; @@ -9,18 +10,20 @@ namespace Ookii.CommandLine; /// /// /// 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. +/// 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 will use the assembly name of the +/// 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. /// /// -/// It is also used by the automatically created "version" command. +/// 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 { diff --git a/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs b/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs index 72af4ace..3f0eda56 100644 --- a/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs +++ b/src/Ookii.CommandLine/ArgumentParsedEventArgs.cs @@ -37,8 +37,8 @@ public CommandLineArgument Argument /// /// /// One of the values of the enumeration. The default value is the - /// value of the attribute, or the - /// return value of a method argument. + /// value of the + /// property, or the return value of a method argument. /// /// /// diff --git a/src/Ookii.CommandLine/CancelMode.cs b/src/Ookii.CommandLine/CancelMode.cs index 7916a53b..f659c973 100644 --- a/src/Ookii.CommandLine/CancelMode.cs +++ b/src/Ookii.CommandLine/CancelMode.cs @@ -1,7 +1,7 @@ namespace Ookii.CommandLine; /// -/// Indicates whether and how the argument should cancel parsing. +/// Indicates whether and how an argument should cancel parsing. /// /// /// diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index d4a4d983..61026393 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1450,7 +1450,7 @@ private IValueHelper CreateValueHelper() { case ArgumentKind.Dictionary: type = typeof(DictionaryValueHelper<,>).MakeGenericType(_elementType.GetGenericArguments()); - return (IValueHelper)Activator.CreateInstance(type, DictionaryInfo!.AllowDuplicateDictionaryKeys, _allowNull)!; + return (IValueHelper)Activator.CreateInstance(type, DictionaryInfo!.AllowDuplicateKeys, _allowNull)!; case ArgumentKind.MultiValue: type = typeof(MultiValueHelper<>).MakeGenericType(_elementTypeWithNullable); diff --git a/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs b/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs index 026253b8..08486313 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentErrorCategory.cs @@ -35,8 +35,7 @@ public enum CommandLineArgumentErrorCategory /// MissingRequiredArgument, /// - /// Invalid value for a dictionary argument; typically the result of a duplicate key or - /// a value without a key/value separator. + /// Invalid value for a dictionary argument; typically the result of a duplicate key. /// InvalidDictionaryValue, /// @@ -57,13 +56,13 @@ public enum CommandLineArgumentErrorCategory /// CombinedShortNameNonSwitch, /// - /// An instance of a class derived from the + /// 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. + /// 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 64d36b60..1713f348 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentException.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentException.cs @@ -8,13 +8,13 @@ namespace Ookii.CommandLine; /// /// /// -/// This exception indicates that the command line passed to the method -/// was invalid for the arguments defined by the instance. +/// This exception indicates that the command line passed to the +/// method, or +/// another parsing 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. +/// Use the property to determine the exact cause of the exception. /// /// /// @@ -36,7 +36,8 @@ public CommandLineArgumentException() { } public CommandLineArgumentException(string? message) : base(message) { } /// - /// Initializes a new instance of the class with a specified error message. + /// 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. @@ -95,7 +96,10 @@ public CommandLineArgumentException(string? message, CommandLineArgumentErrorCat /// that is the cause of this . /// /// The error message that explains the reason for the . - /// The name of the argument that was invalid. + /// + /// 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 , @@ -142,7 +146,8 @@ public CommandLineArgumentErrorCategory Category } /// - /// Sets the object with the parameter name and additional exception information. + /// 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. diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index d437221b..c7a9b49f 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -517,7 +517,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - public CultureInfo Culture => _parseOptions.CultureOrDefault; + public CultureInfo Culture => _parseOptions.Culture; /// /// Gets a value indicating whether duplicate arguments are allowed. diff --git a/src/Ookii.CommandLine/DescriptionListFilterMode.cs b/src/Ookii.CommandLine/DescriptionListFilterMode.cs index 567ac9fb..8b5284aa 100644 --- a/src/Ookii.CommandLine/DescriptionListFilterMode.cs +++ b/src/Ookii.CommandLine/DescriptionListFilterMode.cs @@ -1,7 +1,7 @@ namespace Ookii.CommandLine; /// -/// Indicates which arguments should be included in the description list when printing usage. +/// Indicates which arguments should be included in the description list when generating usage help. /// /// public enum DescriptionListFilterMode diff --git a/src/Ookii.CommandLine/DescriptionListSortMode.cs b/src/Ookii.CommandLine/DescriptionListSortMode.cs index 0a565cf7..c32ae5a5 100644 --- a/src/Ookii.CommandLine/DescriptionListSortMode.cs +++ b/src/Ookii.CommandLine/DescriptionListSortMode.cs @@ -1,15 +1,15 @@ namespace Ookii.CommandLine; /// -/// Indicates how the arguments in the description list should be sorted. +/// Indicates how the arguments in the description list should be sorted when generating usage help. /// /// 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. + /// arguments, then the non-positional required named arguments sorted by name, then the + /// remaining arguments sorted by name. /// UsageOrder, /// diff --git a/src/Ookii.CommandLine/DictionaryArgumentInfo.cs b/src/Ookii.CommandLine/DictionaryArgumentInfo.cs index 34cdd19a..0456e800 100644 --- a/src/Ookii.CommandLine/DictionaryArgumentInfo.cs +++ b/src/Ookii.CommandLine/DictionaryArgumentInfo.cs @@ -12,7 +12,7 @@ public sealed class DictionaryArgumentInfo /// /// Initializes a new instance of the class. /// - /// + /// /// if duplicate dictionary keys are allowed; otherwise, /// . /// @@ -23,22 +23,22 @@ public sealed class DictionaryArgumentInfo /// or or /// is . /// - public DictionaryArgumentInfo(bool allowDuplicateDictionaryKeys, Type keyType, Type valueType, string keyValueSeparator) + public DictionaryArgumentInfo(bool allowDuplicateKeys, Type keyType, Type valueType, string keyValueSeparator) { - AllowDuplicateDictionaryKeys = allowDuplicateDictionaryKeys; + 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, if it is a dictionary argument, allows duplicate keys. + /// Gets a value indicating whether this argument allows duplicate keys. /// /// /// if this argument allows duplicate keys; otherwise, . /// /// - public bool AllowDuplicateDictionaryKeys { get; } + public bool AllowDuplicateKeys { get; } /// /// Gets the type of the keys of a dictionary argument. @@ -57,7 +57,7 @@ public DictionaryArgumentInfo(bool allowDuplicateDictionaryKeys, Type keyType, T public Type ValueType { get; } /// - /// Gets the separator for key/value pairs if this argument is a dictionary argument. + /// Gets the separator for key/value pairs. /// /// /// The custom value specified using the attribute, or diff --git a/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs b/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs index 14f55487..54ca148d 100644 --- a/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs +++ b/src/Ookii.CommandLine/DuplicateArgumentEventArgs.cs @@ -5,6 +5,7 @@ namespace Ookii.CommandLine; /// /// Provides data for the event. /// +/// public class DuplicateArgumentEventArgs : EventArgs { private readonly CommandLineArgument _argument; @@ -16,7 +17,9 @@ public class DuplicateArgumentEventArgs : EventArgs /// Initializes a new instance of the class. /// /// The argument that was specified more than once. - /// The new value of the argument. + /// + /// The raw new value of the argument, or if the argument has no value. + /// /// /// is /// @@ -32,7 +35,7 @@ public DuplicateArgumentEventArgs(CommandLineArgument argument, string? newValue /// /// The argument that was specified more than once. /// if the argument has a value; otherwise, . - /// The new value of the argument. + /// The raw new value of the argument. /// /// is /// @@ -55,7 +58,8 @@ public DuplicateArgumentEventArgs(CommandLineArgument argument, bool hasValue, R /// Gets the new value that will be assigned to the argument. /// /// - /// The raw string value provided on the command line, before conversion. + /// 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; diff --git a/src/Ookii.CommandLine/GeneratedParserAttribute.cs b/src/Ookii.CommandLine/GeneratedParserAttribute.cs index edb4f93f..e35afc25 100644 --- a/src/Ookii.CommandLine/GeneratedParserAttribute.cs +++ b/src/Ookii.CommandLine/GeneratedParserAttribute.cs @@ -14,27 +14,33 @@ namespace Ookii.CommandLine; /// /// /// To use the generated parser, source generation will add several static methods to the target -/// class: CreateParser, and three overloads of the Parse method. Using these -/// members allows trimming your application without warnings, as they avoid the regular -/// constructors of the and +/// 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. +/// When using source generation with subcommands, you should also use a class with the +/// attribute to access the commands. /// /// +/// /// Source generation [AttributeUsage(AttributeTargets.Class)] public sealed class GeneratedParserAttribute : Attribute { /// - /// Gets or sets a value that indicates whether to generate static Parse methods for the - /// arguments class. + /// Gets or sets a value that indicates whether to generate an implementation of the + /// interface for the arguments class. /// /// - /// to generate static Parse methods; otherwise, . - /// The default value is , but see the remarks. + /// to generate an implementation of the + /// interface; otherwise, . The default value is , + /// but see the remarks. /// /// /// @@ -44,11 +50,16 @@ public sealed class GeneratedParserAttribute : Attribute /// the interface on the class. /// /// - /// The default behavior is to generate the static Parse methods unless this property - /// is explicitly set to . However, if the class is a command (it - /// implements the interface and has the - /// attribute), the default is to not generate the static Parse methods - /// unless this property is explicitly set to . + /// 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 index 5d3f4b00..b9542fbe 100644 --- a/src/Ookii.CommandLine/IParser.cs +++ b/src/Ookii.CommandLine/IParser.cs @@ -13,16 +13,16 @@ namespace Ookii.CommandLine; /// This type is only available when using .Net 7 or later. /// /// -/// This interface is automatically implemented on a class (on .Net 7 and later only) when the +/// 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, the -/// will still generate the same methods defined by this -/// interface, just without having them implement the interface. +/// 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 @@ -30,7 +30,8 @@ public interface IParser : IParserProvider { /// /// Parses the arguments returned by the - /// method using the type . + /// method using the type , handling errors and showing usage help + /// as required. /// /// /// The options that control parsing behavior and usage help formatting. If @@ -45,7 +46,8 @@ public interface IParser : IParserProvider public static abstract TSelf? Parse(ParseOptions? options = null); /// - /// Parses the specified command line arguments using the type . + /// Parses the specified command line arguments using the type , + /// handling errors and showing usage help as required. /// /// The command line arguments. /// @@ -60,21 +62,8 @@ public interface IParser : IParserProvider /// public static abstract TSelf? Parse(string[] args, ParseOptions? options = null); - /// - /// Parses the specified command line arguments, starting at the specified index, using the - /// type . - /// - /// 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 . - /// - /// + /// + /// public static abstract TSelf? Parse(ReadOnlyMemory args, ParseOptions? options = null); } diff --git a/src/Ookii.CommandLine/IParserProvider.cs b/src/Ookii.CommandLine/IParserProvider.cs index d5bafb1c..c9a98fd1 100644 --- a/src/Ookii.CommandLine/IParserProvider.cs +++ b/src/Ookii.CommandLine/IParserProvider.cs @@ -13,16 +13,15 @@ namespace Ookii.CommandLine; /// This type is only available when using .Net 7 or later. /// /// -/// This interface is automatically implemented on a class (on .Net 7 and later only) when the +/// 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 directly; these classes do not support this interface unless it is manually -/// implemented. +/// 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, the -/// will still generate the same method defined by this -/// interface, just without having it implement the interface. +/// 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 diff --git a/src/Ookii.CommandLine/LineWrappingTextWriter.cs b/src/Ookii.CommandLine/LineWrappingTextWriter.cs index feab9be1..d7a27f24 100644 --- a/src/Ookii.CommandLine/LineWrappingTextWriter.cs +++ b/src/Ookii.CommandLine/LineWrappingTextWriter.cs @@ -12,7 +12,8 @@ namespace Ookii.CommandLine; /// /// Implements a that writes text to another , -/// white-space wrapping lines at the specified maximum line length, and supporting indentation. +/// white-space wrapping lines at the specified maximum line length, and supporting hanging +/// indentation. /// /// /// @@ -31,12 +32,16 @@ namespace Ookii.CommandLine; /// /// /// 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. +/// 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. The length of the indentation counts towards the maximum line length. +/// 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 @@ -348,13 +353,18 @@ public TextWriter BaseWriter get { return _baseWriter; } } - /// - public override Encoding Encoding - { - get { return _baseWriter.Encoding; } - } + /// + /// 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] #endif @@ -388,11 +398,12 @@ public int MaximumLineLength /// /// /// Whenever a line break is encountered (either because of wrapping or because a line break was written to the - /// , the next line is indented by the number of characters specified - /// by the property. + /// ), 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 output position can be reset to the start of the line after a line break by calling + /// the method. /// /// public int Indent @@ -461,7 +472,10 @@ public WrappingMode Wrapping /// Gets a that writes to the standard output stream, /// using as the maximum line length. /// - /// A that writes to the standard output stream. + /// + /// A that writes to , + /// the standard output stream. + /// public static LineWrappingTextWriter ForConsoleOut() { return new LineWrappingTextWriter(Console.Out, GetLineLengthForConsole(), false); @@ -471,7 +485,10 @@ public static LineWrappingTextWriter ForConsoleOut() /// Gets a that writes to the standard error stream, /// using as the maximum line length. ///
- /// A that writes to the standard error stream. + /// + /// A that writes to , + /// the standard error stream. + /// public static LineWrappingTextWriter ForConsoleError() { return new LineWrappingTextWriter(Console.Error, GetLineLengthForConsole(), false); @@ -491,8 +508,8 @@ public static LineWrappingTextWriter ForConsoleError() /// /// A that writes to a . /// - /// To retrieve the resulting string, first call , then use the method of the . + /// 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) { diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs b/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs index 29a4ec6f..471dc8c1 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Error.cs @@ -56,7 +56,7 @@ public virtual string MissingNamedArgumentValue(string argumentName) /// or property is . ///
/// The name of the argument. - /// The error message. + /// The warning message. public virtual string DuplicateArgumentWarning(string argumentName) => Format(Resources.DuplicateArgumentWarningFormat, argumentName); /// @@ -78,7 +78,7 @@ public virtual string MissingRequiredArgument(string argumentName) /// /// The name of the argument. /// The value of the argument. - /// The error message of the conversion. + /// 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); @@ -86,7 +86,7 @@ public virtual string InvalidDictionaryValue(string argumentName, string? argume /// /// Gets the error message for . /// - /// The error message of the conversion. + /// The error message from instantiating the type. /// The error message. public virtual string CreateArgumentsTypeError(string? message) => Format(Resources.CreateArgumentsTypeErrorFormat, message); @@ -95,7 +95,7 @@ public virtual string CreateArgumentsTypeError(string? message) /// Gets the error message for . ///
/// The name of the argument. - /// The error message of the conversion. + /// The error message from setting the value. /// The error message. public virtual string ApplyValueError(string argumentName, string? message) => Format(Resources.SetValueErrorFormat, argumentName, message); diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs b/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs index 44d602af..51285244 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.Validators.cs @@ -21,7 +21,8 @@ public partial class LocalizedStringProvider /// value to start with a white-space character. /// /// - /// If you override the method, this method will not be called. + /// If you override the + /// method, this method will not be called. /// /// public virtual string ValidatorDescriptions(CommandLineArgument argument) @@ -40,7 +41,7 @@ public virtual string ValidatorDescriptions(CommandLineArgument argument) } /// - /// Gets the usage help for the class. + /// Gets the usage help for the attribute. /// /// The attribute instance. /// The string. @@ -59,21 +60,21 @@ public virtual string ValidateCountUsageHelp(ValidateCountAttribute attribute) } /// - /// Gets the usage help for the class. + /// Gets the usage help for the attribute. /// /// The string. public virtual string ValidateNotEmptyUsageHelp() => Resources.ValidateNotEmptyUsageHelp; /// - /// Gets the usage help for the class. + /// Gets the usage help for the attribute. /// /// The string. public virtual string ValidateNotWhiteSpaceUsageHelp() => Resources.ValidateNotWhiteSpaceUsageHelp; /// - /// Gets the usage help for the class. + /// Gets the usage help for the attribute. /// /// The attribute instance. /// The string. @@ -92,7 +93,7 @@ public virtual string ValidateRangeUsageHelp(ValidateRangeAttribute attribute) } /// - /// Gets the usage help for the class. + /// Gets the usage help for the attribute. /// /// The attribute instance. /// The string. @@ -111,7 +112,7 @@ public virtual string ValidateStringLengthUsageHelp(ValidateStringLengthAttribut } /// - /// Gets the usage help for the class. + /// Gets the usage help for the attribute. /// /// The enumeration type. /// The string. @@ -120,7 +121,7 @@ public virtual string ValidateEnumValueUsageHelp(Type enumType) /// - /// Gets the usage help for the class. + /// Gets the usage help for the attribute. /// /// The prohibited arguments. /// The string. @@ -129,7 +130,7 @@ public virtual string ProhibitsUsageHelp(IEnumerable argume string.Join(ArgumentSeparator, arguments.Select(a => a.ArgumentNameWithPrefix))); /// - /// Gets the usage help for the class. + /// Gets the usage help for the attribute. /// /// The required arguments. /// The string. @@ -138,10 +139,10 @@ public virtual string RequiresUsageHelp(IEnumerable argumen string.Join(ArgumentSeparator, arguments.Select(a => a.ArgumentNameWithPrefix))); /// - /// Gets an error message used if the fails validation. + /// Gets the usage help for the attribute. /// /// The names of the arguments. - /// The error message. + /// The string. public virtual string RequiresAnyUsageHelp(IEnumerable arguments) { // This deliberately reuses the error messge. @@ -164,7 +165,8 @@ public virtual string ValidationFailed(string argumentName) public virtual string ClassValidationFailed() => Resources.ClassValidationFailed; /// - /// Gets an error message used if the fails validation. + /// Gets an error message used if the attribute fails + /// validation. /// /// The name of the argument. /// The . @@ -186,7 +188,7 @@ public virtual string ValidateRangeFailed(string argumentName, ValidateRangeAttr } /// - /// Gets an error message used if the fails + /// Gets an error message used if the attribute fails /// validation because the string was empty. /// /// The name of the argument. @@ -202,7 +204,7 @@ public virtual string ValidateNotEmptyFailed(string argumentName) /// /// Gets an error message used if the fails - /// validation because the string was empty. + /// validation because the string was empty or white-space. /// /// The name of the argument. /// The error message. @@ -216,7 +218,8 @@ public virtual string ValidateNotWhiteSpaceFailed(string argumentName) => Format(Resources.ValidateNotWhiteSpaceFailedFormat, argumentName); /// - /// Gets an error message used if the fails validation. + /// Gets an error message used if the attribute + /// fails validation. /// /// The name of the argument. /// The . @@ -238,7 +241,8 @@ public virtual string ValidateStringLengthFailed(string argumentName, ValidateSt } /// - /// Gets an error message used if the fails validation. + /// Gets an error message used if the attribute fails + /// validation. /// /// The name of the argument. /// The . @@ -260,7 +264,8 @@ public virtual string ValidateCountFailed(string argumentName, ValidateCountAttr } /// - /// Gets an error message used if the fails validation. + /// Gets an error message used if the attribute fails + /// validation. /// /// The name of the argument. /// The type of the enumeration. @@ -279,7 +284,8 @@ public virtual string ValidateEnumValueFailed(string argumentName, Type enumType } /// - /// Gets an error message used if the fails validation. + /// Gets an error message used if the attribute fails + /// validation. /// /// The name of the argument. /// The names of the required arguments. @@ -289,7 +295,8 @@ public virtual string ValidateRequiresFailed(string argumentName, IEnumerable a.ArgumentNameWithPrefix))); /// - /// Gets an error message used if the fails validation. + /// Gets an error message used if the attribute fails + /// validation. /// /// The name of the argument. /// The names of the prohibited arguments. @@ -299,7 +306,8 @@ public virtual string ValidateProhibitsFailed(string argumentName, IEnumerable a.ArgumentNameWithPrefix))); /// - /// Gets an error message used if the fails validation. + /// Gets an error message used if the attribute fails + /// validation. /// /// The names of the arguments. /// The error message. diff --git a/src/Ookii.CommandLine/LocalizedStringProvider.cs b/src/Ookii.CommandLine/LocalizedStringProvider.cs index 73e9fd4a..b2ccec0f 100644 --- a/src/Ookii.CommandLine/LocalizedStringProvider.cs +++ b/src/Ookii.CommandLine/LocalizedStringProvider.cs @@ -11,7 +11,8 @@ namespace Ookii.CommandLine; /// /// /// Inherit from this class and override its members to provide customized or localized -/// strings. You can specify the implementation to use using . +/// strings. You can specify the implementation to use with the +/// property. /// /// /// For error messages, this only lets you customize error messages for the @@ -20,6 +21,7 @@ namespace Ookii.CommandLine; /// correct program, and should therefore not be shown to the user. /// /// +/// public partial class LocalizedStringProvider { /// @@ -37,8 +39,9 @@ public partial class LocalizedStringProvider /// /// /// 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. + /// 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 , @@ -94,8 +97,8 @@ public partial class LocalizedStringProvider /// The string. /// /// - /// The base implementation uses the , - /// and will fall back to the assembly version if none is defined. + /// 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) diff --git a/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs b/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs index ea22643f..e3a91082 100644 --- a/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs +++ b/src/Ookii.CommandLine/MultiValueSeparatorAttribute.cs @@ -29,7 +29,7 @@ namespace Ookii.CommandLine; /// /// /// Using white-space separators will not work if the -/// is or if the argument is a multi-value switch argument. +/// property is or if the argument is a multi-value switch argument. /// /// /// Using the constructor, you instead @@ -47,6 +47,7 @@ namespace Ookii.CommandLine; /// -Sample Value1,Value2 -Sample Value3 will mean the argument "Sample" has three values. /// /// +/// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] public class MultiValueSeparatorAttribute : Attribute { diff --git a/src/Ookii.CommandLine/NameTransform.cs b/src/Ookii.CommandLine/NameTransform.cs index 91f722ff..b19749dc 100644 --- a/src/Ookii.CommandLine/NameTransform.cs +++ b/src/Ookii.CommandLine/NameTransform.cs @@ -1,8 +1,8 @@ namespace Ookii.CommandLine; /// -/// Indicates how to transform the property, parameter, or method name if an argument doesn't -/// have an explicit name. +/// 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. /// /// /// diff --git a/src/Ookii.CommandLine/NameTransformExtensions.cs b/src/Ookii.CommandLine/NameTransformExtensions.cs index a1ef0e53..cf5c4e45 100644 --- a/src/Ookii.CommandLine/NameTransformExtensions.cs +++ b/src/Ookii.CommandLine/NameTransformExtensions.cs @@ -6,6 +6,7 @@ namespace Ookii.CommandLine; /// /// Extension methods for the enumeration. /// +/// public static class NameTransformExtensions { /// @@ -15,7 +16,7 @@ public static class NameTransformExtensions /// The name to transform. /// /// An optional suffix to remove from the string before transformation. Only used if - /// is not . + /// is not . /// /// The transformed name. /// diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 06f4e7f9..2ca3aab7 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -9,8 +9,7 @@ namespace Ookii.CommandLine; /// -/// Provides options for the -/// method and the constructor. +/// Provides options that control parsing behavior. /// /// /// @@ -20,30 +19,34 @@ namespace Ookii.CommandLine; /// 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 from their string representation to the argument type, or - /// to use . The default value is - /// - /// - public CultureInfo? Culture { get; set; } - - /// - /// Gets the culture used to convert command line argument values from their string + /// Gets or sets the culture used to convert command line argument values from their string /// representation to the argument type. /// /// - /// The value of the property, or - /// if that property is . + /// The culture used to convert command line argument values. The default value is + /// . /// - public CultureInfo CultureOrDefault => Culture ?? CultureInfo.InvariantCulture; + /// +#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. @@ -145,8 +148,8 @@ public virtual bool IsPosix /// /// /// 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 transform. + /// 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 @@ -183,9 +186,9 @@ public virtual bool IsPosix /// /// /// - /// If the parsing mode is set to , either using the - /// property or the attribute, - /// this property sets the short argument name prefixes. Use the + /// 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. /// /// @@ -278,14 +281,22 @@ public virtual bool IsPosix /// 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 . /// + /// + /// + /// 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; } /// @@ -305,11 +316,13 @@ public virtual bool IsPosix /// /// /// 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. + /// 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 @@ -373,14 +386,14 @@ public virtual bool IsPosix /// if the default value is used. /// /// - /// The character chosen here cannot be used in the name of any parameter. Therefore, + /// 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 character can appear in argument values (e.g. -sample:foo:bar is fine, in which - /// case the value is "foo:bar"). + /// 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 - /// whitespace character is part of the argument, which usually means it needs to be + /// 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. @@ -544,6 +557,9 @@ public virtual bool IsPosix /// /// /// + /// 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. /// @@ -557,6 +573,9 @@ public virtual bool IsPosix /// property will be written to undo the color change. /// /// + /// + /// + /// public string ErrorColor { get; set; } = TextFormat.ForegroundRed; /// @@ -568,6 +587,9 @@ public virtual bool IsPosix /// /// /// + /// 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. /// @@ -585,6 +607,9 @@ public virtual bool IsPosix /// property will be written to undo the color change. /// /// + /// + /// + /// public string WarningColor { get; set; } = TextFormat.ForegroundYellow; /// @@ -596,12 +621,12 @@ public virtual bool IsPosix /// /// /// + /// Only the parsing methods that automatically handle errors will use this property. + /// + /// /// 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. + /// , 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 @@ -609,6 +634,9 @@ public virtual bool IsPosix /// garbage characters appearing in the output. /// /// + /// + /// + /// public bool? UseErrorColor { get; set; } /// @@ -627,6 +655,9 @@ public virtual bool IsPosix /// /// /// +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + [AllowNull] +#endif public LocalizedStringProvider StringProvider { get => _stringProvider ??= new LocalizedStringProvider(); @@ -642,13 +673,17 @@ public LocalizedStringProvider StringProvider /// /// /// - /// 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. + /// 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; } /// @@ -685,8 +720,8 @@ public LocalizedStringProvider StringProvider /// /// /// This property has no effect on explicit value description specified with the - /// property or the - /// property. + /// attribute or the + /// property. /// /// /// If not , this property overrides the @@ -718,9 +753,10 @@ public LocalizedStringProvider StringProvider /// /// 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 CreateParser and Parse methods on the command line - /// arguments type, the generated parser is used regardless of the value of this property. + /// 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; @@ -733,7 +769,9 @@ public LocalizedStringProvider StringProvider /// Gets or sets the to use to create usage help. /// /// - /// An instance of the class. + /// 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] From 868295ed12e76a149cbc14baf521ddbc2bd611a3 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 22 Jun 2023 17:12:35 -0700 Subject: [PATCH 184/234] Enable source link. --- src/Ookii.CommandLine/Ookii.CommandLine.csproj | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index 050c31fc..caf35a26 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -26,6 +26,8 @@ true snupkg icon.png + true + true @@ -34,6 +36,11 @@ CS1574 + + + true + + @@ -45,6 +52,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 1663929a5fc4a0cb5ec6bdbb04e72ddf07086d57 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 22 Jun 2023 17:30:31 -0700 Subject: [PATCH 185/234] Update .Net version in workflow. --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 9b3c74cfc67a34cb2a7f27500819c4d4044caf7b Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 22 Jun 2023 18:21:48 -0700 Subject: [PATCH 186/234] Use deterministic build and publish packages. --- src/Create-Release.ps1 | Bin 2846 -> 5164 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/Create-Release.ps1 b/src/Create-Release.ps1 index fae96c8af3458f87489994bef74afbeb93cb8796..23dbf60cf39d2bee3d6208513423e51058bd154e 100644 GIT binary patch literal 5164 zcmcIo+iu%N5S`}${Re>-(A-G&t$>RFO&l~xQ6z?CG!H=@bfIL%wxmi_Whc$Aw>@V@ z>)j=(3k(N>B$7Kj*E5&pzyHaRbmUwvq?9GDmNJwDuIEzW&YrFCw2%?*E1BV!6ovG0 zSKzLX??@>t-8qmIR!UG#u`?tLa6I@^8L)}O6hjJ=F3V_x1vSVEZ} z9zzB2P9b9>cGF*g{k=ikF}= z8|rzLA#cJwbN58X0yf^t8(m*PN`@NK#~t�zTSgq^b||$1?AiO=dNHJHONEA^qCK zawe~UGuE6DEHG;++k1=XB@V6Lk)l9tDuIUBXWZpDxVq%-+x_+DR?b#!V#z(|IsPt^ zL(Y_3LJD!))y(y_hjdRs@m2BE!4*GsYV!_rJ+>qCVA@y>Wmxt42Hxxd>`;fv+$E9r zeN(-8#qSZgo`63)4Evc?{tjxg!v(t^F-;s?!#h)am$)yHU)GOC$fF9+NyX0RxzYY@ zy@&Nb#b>A$gP59V&12Vc-8Jrh)HjwDYWc9TI+7!+kMHUk?Os7s&I)gFO@B-@%bm79 zlU`tT0m|ae)AaHwp7>73sB$J?e~uB-ffOQ;y_+#-y%?w@b|3S&O$VL>(b}wQHf0c- z&9x=!VI5B%`AzyJwL^X!k+`#3gr~45jS#gfR)^H~L$JgC>nA7Eq-j^XcZ{0Ru7Cpm zTHg-4Y~Jcu%Ub6f%Qc^IyK%4tBCa8iS`+JCV9h^qUCMU^&a|xDezHkh=at>w?nN?8 z|2BMux_D%TPunonMXp%4b^RjTkztmxRod&kR-cbv0{s*C!qwCmV~IRRzhWh+;t{NT4PB=4qh_9+ zyEwV8wI;Gp#TcOW6FhaNCRaP-I6emjCz&bgE-UL=e`&Rov_|w;+ucbpmO|8XjlK8^ zve@fv+U+Y>p6skXJHPCUB>Vk9x%g8TXRrIm)wKP4&)XWkLwKZcbyQS6IE zT-$%GdEMnDV;9#ro&x52R+>Q43^r1t`HyEX&WhaS==^!b>g=|eA;-Q9otZt6F`Q~F zGH@ILwXEe~g}$GJ9=P%}&Z)Hm{~X#AGr6A2d`CH?Pi3eFF}Doeo*2B@qAztO{!b7y z_IJ)1_7GKBcOP;BsY#Irst z7Uz%Y<}AZW?hwliImzZE%o%Y8jw{uW85B-&Zl)w}&NO^iL62It@@<2gp)5lS&>Ta5 zR&Qq!PuV>@Z#!eMzlCwnH@=Jg)zp}t^Y27#=46+#`O8mNVaIwX4eNcvqTQykLop`< zQZ4aGg}KM+vDE0bO(*s?fxd~WnCn#g-}^MrCt37n#becFd;FejsW~3k5JzDe)1vx& eEH~TLySdF|R&m=;3uo|-&(Cc$T$?u(8~+1yxmY3q delta 449 zcmZ3ZF;8s5)%pU4M1~?D&SlVG;AP-qP=LZ{282v1LkUAFP?ZLQFHmJ3Lkf^AVaNxH zR5I8yr~uhT45dI7IzYVvK=ERROrTgMkd+6NH(=0Yh-HWdih|T-0>#pSd=-Wekj)}M z4#@Zbu#p*HCumM~V0N6mnO|&zyTd0A#mZszz06;qjfle0_|tQ}3a{$v3b z(aFns_%_#bnK4e*<>Z>YpT}ylDwhbaEzoUBK)XSfnN3#U^~54Nfme01JwKmp0nj%| z3^_oz7Bgf3^{D`9PzV(O#Zo8FVOO5~R#;>*KbM0r7Dr6Z Date: Mon, 26 Jun 2023 12:28:52 -0700 Subject: [PATCH 187/234] Fix incorrect tests. --- src/Ookii.CommandLine.Tests/CommandOptionsTest.cs | 3 ++- src/Ookii.CommandLine.Tests/ParseOptionsTest.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs b/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs index 06dc8661..b48abbd7 100644 --- a/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandOptionsTest.cs @@ -2,6 +2,7 @@ using Ookii.CommandLine.Commands; using Ookii.CommandLine.Terminal; using System; +using System.Globalization; namespace Ookii.CommandLine.Tests; @@ -27,7 +28,7 @@ public void TestConstructor() Assert.IsNull(options.AutoHelpArgument); Assert.IsNull(options.AutoPrefixAliases); Assert.IsNull(options.AutoVersionArgument); - Assert.IsNull(options.Culture); + Assert.AreEqual(CultureInfo.InvariantCulture, options.Culture); Assert.IsNull(options.DefaultValueDescriptions); Assert.IsNull(options.DuplicateArguments); Assert.IsNull(options.Error); diff --git a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs index ad820317..613768e7 100644 --- a/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs +++ b/src/Ookii.CommandLine.Tests/ParseOptionsTest.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Ookii.CommandLine.Terminal; using System; +using System.Globalization; using System.Linq; namespace Ookii.CommandLine.Tests; @@ -26,7 +27,7 @@ public void TestConstructor() Assert.IsNull(options.AutoHelpArgument); Assert.IsNull(options.AutoPrefixAliases); Assert.IsNull(options.AutoVersionArgument); - Assert.IsNull(options.Culture); + Assert.AreEqual(CultureInfo.InvariantCulture, options.Culture); Assert.IsNull(options.DefaultValueDescriptions); Assert.IsNull(options.DuplicateArguments); Assert.IsNull(options.Error); From b9d20831554a1a7e2a270bc022f1e6a80bf2b7ec Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 26 Jun 2023 12:30:03 -0700 Subject: [PATCH 188/234] Don't change console mode if VT already supported. --- src/Ookii.CommandLine/NativeMethods.cs | 15 ++++++++++----- src/Ookii.CommandLine/Terminal/VirtualTerminal.cs | 12 +++++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Ookii.CommandLine/NativeMethods.cs b/src/Ookii.CommandLine/NativeMethods.cs index 17ad25d7..32773241 100644 --- a/src/Ookii.CommandLine/NativeMethods.cs +++ b/src/Ookii.CommandLine/NativeMethods.cs @@ -8,7 +8,7 @@ static class NativeMethods { 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) { @@ -18,12 +18,12 @@ static class NativeMethods var handle = GetStandardHandle(stream); if (handle == INVALID_HANDLE_VALUE) { - return null; + return (false, null); } if (!GetConsoleMode(handle, out ConsoleModes mode)) { - return null; + return (false, null); } var oldMode = mode; @@ -36,12 +36,17 @@ static class NativeMethods mode &= ~ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING; } + if (oldMode == mode) + { + return (true, null); + } + if (!SetConsoleMode(handle, mode)) { - return null; + return (false, null); } - return oldMode; + return (true, oldMode); } public static IntPtr GetStandardHandle(StandardStream stream) diff --git a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs index 366de22e..b931a1e0 100644 --- a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs +++ b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs @@ -62,13 +62,19 @@ public static VirtualTerminalSupport EnableVirtualTerminalSequences(StandardStre if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var previousMode = NativeMethods.EnableVirtualTerminalSequences(stream, true); - if (previousMode == null) + var (enabled, previousMode) = NativeMethods.EnableVirtualTerminalSequences(stream, true); + if (!enabled) { return new VirtualTerminalSupport(false); } - return new VirtualTerminalSupport(NativeMethods.GetStandardHandle(stream), previousMode.Value); + if (previousMode is NativeMethods.ConsoleModes mode) + { + return new VirtualTerminalSupport(NativeMethods.GetStandardHandle(stream), mode); + } + + // Support was already enabled externally, so don't change the console mode on dispose. + return new VirtualTerminalSupport(true); } // Support is assumed on non-Windows platforms if TERM is set. From 1d6bfda4f41b0b18df92b20467c2ef46b32f7bf3 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 26 Jun 2023 13:17:47 -0700 Subject: [PATCH 189/234] Strong typed VT sequences. --- src/Ookii.CommandLine.Tests/TextFormatTest.cs | 44 ++++ src/Ookii.CommandLine/CommandLineParser.cs | 3 +- src/Ookii.CommandLine/ParseOptions.cs | 14 +- src/Ookii.CommandLine/Terminal/TextFormat.cs | 192 +++++++++++++----- src/Ookii.CommandLine/UsageWriter.cs | 32 +-- 5 files changed, 200 insertions(+), 85 deletions(-) create mode 100644 src/Ookii.CommandLine.Tests/TextFormatTest.cs 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/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index c7a9b49f..3dc1be37 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -1,5 +1,6 @@ using Ookii.CommandLine.Commands; using Ookii.CommandLine.Support; +using Ookii.CommandLine.Terminal; using Ookii.CommandLine.Validation; using System; using System.Collections.Generic; @@ -1342,7 +1343,7 @@ internal static bool ShouldIndent(LineWrappingTextWriter writer) return writer.MaximumLineLength is 0 or >= 30; } - internal static void WriteError(ParseOptions options, string message, string color, bool blankLine = false) + internal static void WriteError(ParseOptions options, string message, TextFormat color, bool blankLine = false) { using var errorVtSupport = options.EnableErrorColor(); try diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 2ca3aab7..1c416f2e 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -564,11 +564,6 @@ public virtual bool IsPosix /// ; 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. /// @@ -576,7 +571,7 @@ public virtual bool IsPosix /// /// /// - public string ErrorColor { get; set; } = TextFormat.ForegroundRed; + public TextFormat ErrorColor { get; set; } = TextFormat.ForegroundRed; /// /// Gets or sets the color applied to warning messages. @@ -598,11 +593,6 @@ public virtual bool IsPosix /// 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. /// @@ -610,7 +600,7 @@ public virtual bool IsPosix /// /// /// - public string WarningColor { get; set; } = TextFormat.ForegroundYellow; + public TextFormat WarningColor { get; set; } = TextFormat.ForegroundYellow; /// /// Gets or sets a value that indicates whether error messages should use color. diff --git a/src/Ookii.CommandLine/Terminal/TextFormat.cs b/src/Ookii.CommandLine/Terminal/TextFormat.cs index d88fd449..b554d3ad 100644 --- a/src/Ookii.CommandLine/Terminal/TextFormat.cs +++ b/src/Ookii.CommandLine/Terminal/TextFormat.cs @@ -4,174 +4,192 @@ namespace Ookii.CommandLine.Terminal; /// -/// Provides constants for various virtual terminal sequences that control text format. +/// Represents a virtual terminal (VT) sequence for a change in text formatting. /// -public static class TextFormat +/// +/// +/// Write one of the predefined values in this class 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 { /// /// Resets the text format to the settings before modification. /// - public const string Default = "\x1b[0m"; + public static readonly TextFormat Default = new("\x1b[0m"); /// /// Applies the brightness/intensity flag to the foreground color. /// - public const string BoldBright = "\x1b[1m"; + public static readonly TextFormat BoldBright = new("\x1b[1m"); /// /// Removes the brightness/intensity flag to the foreground color. /// - public const string NoBoldBright = "\x1b[22m"; + public static readonly TextFormat NoBoldBright = new("\x1b[22m"); /// /// Adds underline. /// - public const string Underline = "\x1b[4m"; + public static readonly TextFormat Underline = new("\x1b[4m"); /// /// Removes underline. /// - public const string NoUnderline = "\x1b[24m"; + public static readonly TextFormat NoUnderline = new("\x1b[24m"); /// /// Swaps foreground and background colors. /// - public const string Negative = "\x1b[7m"; + public static readonly TextFormat Negative = new("\x1b[7m"); /// /// Returns foreground and background colors to normal. /// - public const string Positive = "\x1b[27m"; + public static readonly TextFormat Positive = new("\x1b[27m"); /// /// Sets the foreground color to Black. /// - public const string ForegroundBlack = "\x1b[30m"; + public static readonly TextFormat ForegroundBlack = new("\x1b[30m"); /// /// Sets the foreground color to Red. /// - public const string ForegroundRed = "\x1b[31m"; + public static readonly TextFormat ForegroundRed = new("\x1b[31m"); /// /// Sets the foreground color to Green. /// - public const string ForegroundGreen = "\x1b[32m"; + public static readonly TextFormat ForegroundGreen = new("\x1b[32m"); /// /// Sets the foreground color to Yellow. /// - public const string ForegroundYellow = "\x1b[33m"; + public static readonly TextFormat ForegroundYellow = new("\x1b[33m"); /// /// Sets the foreground color to Blue. /// - public const string ForegroundBlue = "\x1b[34m"; + public static readonly TextFormat ForegroundBlue = new("\x1b[34m"); /// /// Sets the foreground color to Magenta. /// - public const string ForegroundMagenta = "\x1b[35m"; + public static readonly TextFormat ForegroundMagenta = new("\x1b[35m"); /// /// Sets the foreground color to Cyan. /// - public const string ForegroundCyan = "\x1b[36m"; + public static readonly TextFormat ForegroundCyan = new("\x1b[36m"); /// /// Sets the foreground color to White. /// - public const string ForegroundWhite = "\x1b[37m"; + public static readonly TextFormat ForegroundWhite = new("\x1b[37m"); /// /// Sets the foreground color to Default. /// - public const string ForegroundDefault = "\x1b[39m"; + public static readonly TextFormat ForegroundDefault = new("\x1b[39m"); /// /// Sets the background color to Black. /// - public const string BackgroundBlack = "\x1b[40m"; + public static readonly TextFormat BackgroundBlack = new("\x1b[40m"); /// /// Sets the background color to Red. /// - public const string BackgroundRed = "\x1b[41m"; + public static readonly TextFormat BackgroundRed = new("\x1b[41m"); /// /// Sets the background color to Green. /// - public const string BackgroundGreen = "\x1b[42m"; + public static readonly TextFormat BackgroundGreen = new("\x1b[42m"); /// /// Sets the background color to Yellow. /// - public const string BackgroundYellow = "\x1b[43m"; + public static readonly TextFormat BackgroundYellow = new("\x1b[43m"); /// /// Sets the background color to Blue. /// - public const string BackgroundBlue = "\x1b[44m"; + public static readonly TextFormat BackgroundBlue = new("\x1b[44m"); /// /// Sets the background color to Magenta. /// - public const string BackgroundMagenta = "\x1b[45m"; + public static readonly TextFormat BackgroundMagenta = new("\x1b[45m"); /// /// Sets the background color to Cyan. /// - public const string BackgroundCyan = "\x1b[46m"; + public static readonly TextFormat BackgroundCyan = new("\x1b[46m"); /// /// Sets the background color to White. /// - public const string BackgroundWhite = "\x1b[47m"; + public static readonly TextFormat BackgroundWhite = new("\x1b[47m"); /// /// Sets the background color to Default. /// - public const string BackgroundDefault = "\x1b[49m"; + public static readonly TextFormat BackgroundDefault = new("\x1b[49m"); /// /// Sets the foreground color to bright Black. /// - public const string BrightForegroundBlack = "\x1b[90m"; + public static readonly TextFormat BrightForegroundBlack = new("\x1b[90m"); /// /// Sets the foreground color to bright Red. /// - public const string BrightForegroundRed = "\x1b[91m"; + public static readonly TextFormat BrightForegroundRed = new("\x1b[91m"); /// /// Sets the foreground color to bright Green. /// - public const string BrightForegroundGreen = "\x1b[92m"; + public static readonly TextFormat BrightForegroundGreen = new("\x1b[92m"); /// /// Sets the foreground color to bright Yellow. /// - public const string BrightForegroundYellow = "\x1b[93m"; + public static readonly TextFormat BrightForegroundYellow = new("\x1b[93m"); /// /// Sets the foreground color to bright Blue. /// - public const string BrightForegroundBlue = "\x1b[94m"; + public static readonly TextFormat BrightForegroundBlue = new("\x1b[94m"); /// /// Sets the foreground color to bright Magenta. /// - public const string BrightForegroundMagenta = "\x1b[95m"; + public static readonly TextFormat BrightForegroundMagenta = new("\x1b[95m"); /// /// Sets the foreground color to bright Cyan. /// - public const string BrightForegroundCyan = "\x1b[96m"; + public static readonly TextFormat BrightForegroundCyan = new("\x1b[96m"); /// /// Sets the foreground color to bright White. /// - public const string BrightForegroundWhite = "\x1b[97m"; + public static readonly TextFormat BrightForegroundWhite = new("\x1b[97m"); /// /// Sets the background color to bright Black. /// - public const string BrightBackgroundBlack = "\x1b[100m"; + public static readonly TextFormat BrightBackgroundBlack = new("\x1b[100m"); /// /// Sets the background color to bright Red. /// - public const string BrightBackgroundRed = "\x1b[101m"; + public static readonly TextFormat BrightBackgroundRed = new("\x1b[101m"); /// /// Sets the background color to bright Green. /// - public const string BrightBackgroundGreen = "\x1b[102m"; + public static readonly TextFormat BrightBackgroundGreen = new("\x1b[102m"); /// /// Sets the background color to bright Yellow. /// - public const string BrightBackgroundYellow = "\x1b[103m"; + public static readonly TextFormat BrightBackgroundYellow = new("\x1b[103m"); /// /// Sets the background color to bright Blue. /// - public const string BrightBackgroundBlue = "\x1b[104m"; + public static readonly TextFormat BrightBackgroundBlue = new("\x1b[104m"); /// /// Sets the background color to bright Magenta. /// - public const string BrightBackgroundMagenta = "\x1b[105m"; + public static readonly TextFormat BrightBackgroundMagenta = new("\x1b[105m"); /// /// Sets the background color to bright Cyan. /// - public const string BrightBackgroundCyan = "\x1b[106m"; + public static readonly TextFormat BrightBackgroundCyan = new("\x1b[106m"); /// /// Sets the background color to bright White. /// - public const string BrightBackgroundWhite = "\x1b[107m"; + public static readonly TextFormat BrightBackgroundWhite = new("\x1b[107m"); + + private readonly string? _value; /// /// Returns the virtual terminal sequence to the foreground or background color to an RGB @@ -182,9 +200,91 @@ public static class TextFormat /// 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) + /// A instance with the virtual terminal sequence. + public static TextFormat GetExtendedColor(Color color, bool foreground = true) + { + 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; + + /// + /// 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) { - return FormattableString.Invariant($"{VirtualTerminal.Escape}[{(foreground ? 38 : 48)};2;{color.R};{color.G};{color.B}m"); + if (obj is TextFormat format) + { + 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); + + /// + /// Determine 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); + + /// + /// Determine 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/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index cef486ea..efe45fad 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -220,11 +220,6 @@ public bool IncludeExecutableExtension /// . /// /// - /// 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. /// @@ -233,7 +228,7 @@ public bool IncludeExecutableExtension /// executable name does not. /// /// - public string UsagePrefixColor { get; set; } = TextFormat.ForegroundCyan; + 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. @@ -348,11 +343,6 @@ public bool IncludeExecutableExtension /// . /// /// - /// 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. /// @@ -361,7 +351,7 @@ public bool IncludeExecutableExtension /// portion of the string has color; the actual argument description does not. /// /// - public string ArgumentDescriptionColor { get; set; } = TextFormat.ForegroundGreen; + public TextFormat ArgumentDescriptionColor { get; set; } = TextFormat.ForegroundGreen; /// /// Gets or sets a value indicating whether white space, rather than the first item of the @@ -451,13 +441,8 @@ public bool IncludeExecutableExtension /// 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; + public TextFormat ColorReset { get; set; } = TextFormat.Default; /// /// Gets or sets the name of the subcommand. @@ -496,11 +481,6 @@ public bool IncludeExecutableExtension /// . /// /// - /// 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 . /// /// @@ -508,7 +488,7 @@ public bool IncludeExecutableExtension /// application name does not. /// /// - public string CommandDescriptionColor { get; set; } = TextFormat.ForegroundGreen; + 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. @@ -2018,11 +1998,11 @@ protected virtual void WriteSpacing(int count) /// Nothing is written if the property is . /// /// - protected void WriteColor(string color) + protected void WriteColor(TextFormat color) { if (UseColor) { - Write(color); + Write(color.Value); } } From 054eee2a7f9c0c33225411ba5b89b0496957aa31 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 26 Jun 2023 14:51:10 -0700 Subject: [PATCH 190/234] Publish package to local nuget feed. --- src/Create-Release.ps1 | Bin 5164 -> 6052 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/Create-Release.ps1 b/src/Create-Release.ps1 index 23dbf60cf39d2bee3d6208513423e51058bd154e..e7337cc3bfcee72e762795c87fc11950c673e5f4 100644 GIT binary patch delta 820 zcmbVK&nrYx6h3BS=8fk)KZ}%UqTWn1L)4V8u=HCbYEaV{Ok<4Z4U&e1ENrMbi&)S#dCU=^ zAv+VNEX7f!k&h-RLL;<73B+PFhjoLfj8PKxv#1-S1)8T-3jE2}WfAHi%YbqL1O{Or zr9mDA%>XS4(yTZnivx`&ec}sr>ezC&CujyuYZS!(G?+X;w&X)@?Q2`SBh~Yvv!&R) zHa|MY_@%_jL09-J;%X>zS$gEyqtx@R6tYXNq%|9d-M!c*@us_lZ%Vy<>weqo(-8Q8nVFb!svjG;Ep0$}sz Date: Mon, 26 Jun 2023 17:11:41 -0700 Subject: [PATCH 191/234] More XML comment updates. --- src/Ookii.CommandLine/ParseOptions.cs | 1 + .../ParseOptionsAttribute.cs | 75 +++--- src/Ookii.CommandLine/ParseResult.cs | 22 +- src/Ookii.CommandLine/ParseStatus.cs | 13 +- src/Ookii.CommandLine/ShortAliasAttribute.cs | 9 +- src/Ookii.CommandLine/Terminal/TextFormat.cs | 2 +- src/Ookii.CommandLine/UsageWriter.cs | 218 +++++++++++------- .../ValueDescriptionAttribute.cs | 14 +- 8 files changed, 207 insertions(+), 147 deletions(-) diff --git a/src/Ookii.CommandLine/ParseOptions.cs b/src/Ookii.CommandLine/ParseOptions.cs index 1c416f2e..8d2615f7 100644 --- a/src/Ookii.CommandLine/ParseOptions.cs +++ b/src/Ookii.CommandLine/ParseOptions.cs @@ -24,6 +24,7 @@ namespace Ookii.CommandLine; /// /// /// +/// public class ParseOptions { private CultureInfo? _culture; diff --git a/src/Ookii.CommandLine/ParseOptionsAttribute.cs b/src/Ookii.CommandLine/ParseOptionsAttribute.cs index 1faea566..c28e0420 100644 --- a/src/Ookii.CommandLine/ParseOptionsAttribute.cs +++ b/src/Ookii.CommandLine/ParseOptionsAttribute.cs @@ -8,24 +8,23 @@ namespace Ookii.CommandLine; /// /// /// -/// 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. +/// 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. /// /// -/// 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. +/// 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 you also use the class, any options provided there will -/// override the options set in this attribute. -/// -/// -/// If you wish to use the default options, you do not need to apply this attribute to your -/// class at all. +/// 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 { @@ -56,7 +55,7 @@ public class ParseOptionsAttribute : 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"). + /// names and value descriptions use dash-case (e.g. "argument-name"). /// /// /// Setting this property to is equivalent to setting the @@ -134,14 +133,14 @@ public virtual bool IsPosix /// /// 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. + /// 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 @@ -184,8 +183,13 @@ public virtual bool IsPosix /// /// /// When , the will use - /// for command line argument comparisons; otherwise, - /// it 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 @@ -210,11 +214,14 @@ public virtual bool IsPosix /// /// /// 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. + /// 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 @@ -230,8 +237,8 @@ public virtual bool IsPosix /// /// /// if white space is allowed to separate an argument name and its - /// value; if only the values from - /// are allowed. The default value is . + /// value; if only the values from tne + /// property are allowed. The default value is . /// /// /// @@ -243,7 +250,7 @@ public virtual bool IsPosix public bool AllowWhiteSpaceValueSeparator { get; set; } = true; /// - /// Gets or sets the character used to separate the name and the value of an argument. + /// 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 @@ -298,7 +305,8 @@ public virtual bool IsPosix /// . /// /// - /// The name, aliases and description can be customized by using a custom . + /// The name, aliases and description can be customized by using a custom + /// class. /// /// /// This value can be overridden by the @@ -332,7 +340,8 @@ public virtual bool IsPosix /// The automatic version argument will never be created for subcommands. /// /// - /// The name and description can be customized by using a custom . + /// The name and description can be customized by using a custom + /// class. /// /// /// This value can be overridden by the @@ -357,9 +366,9 @@ public virtual bool IsPosix /// 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 "Port" would be an alias for "Port, and "Pr" an alias for + /// 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. + /// does not uniquely identify a single argument. /// /// /// When using , this only applies to long names. Explicit @@ -388,7 +397,7 @@ public virtual bool IsPosix /// /// /// This property has no effect on explicit value description specified with the - /// property or the + /// attribute or the /// property. /// /// diff --git a/src/Ookii.CommandLine/ParseResult.cs b/src/Ookii.CommandLine/ParseResult.cs index 9165ed18..18221ad3 100644 --- a/src/Ookii.CommandLine/ParseResult.cs +++ b/src/Ookii.CommandLine/ParseResult.cs @@ -3,10 +3,11 @@ namespace Ookii.CommandLine; /// -/// Indicates the result of the last call to the -/// method. +/// 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, @@ -19,7 +20,7 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception } /// - /// Gets the status of the last call to the + /// Gets the status of the last call to the /// method. /// /// @@ -29,7 +30,7 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception /// /// Gets the exception that occurred during the last call to the - /// method, if any. + /// method, if any. /// /// /// The exception, or if parsing was successful or canceled. @@ -40,10 +41,12 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception /// 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, . + /// 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; } @@ -79,7 +82,8 @@ private ParseResult(ParseStatus status, CommandLineArgumentException? exception /// Gets a instance that represents successful parsing. /// /// - /// The name of the argument that canceled parsing using . + /// The name of the argument that canceled parsing using , + /// or if parsing was not canceled. /// /// Any remaining arguments that were not parsed. /// diff --git a/src/Ookii.CommandLine/ParseStatus.cs b/src/Ookii.CommandLine/ParseStatus.cs index 988c7442..43421548 100644 --- a/src/Ookii.CommandLine/ParseStatus.cs +++ b/src/Ookii.CommandLine/ParseStatus.cs @@ -1,18 +1,21 @@ namespace Ookii.CommandLine; /// -/// Indicates the status of the last call to the -/// method. +/// Indicates the status of the last call to the +/// method or one of its overloads. /// /// public enum ParseStatus { /// - /// The method has not been called yet. + /// The method has not been called yet. /// None, /// - /// The operation was successful. + /// The operation successfully parsed all arguments, or was canceled using + /// . Check the + /// property to differentiate between + /// the two. /// Success, /// @@ -20,7 +23,7 @@ public enum ParseStatus /// Error, /// - /// Parsing was canceled by one of the arguments. + /// Parsing was canceled by one of the arguments using . /// Canceled } diff --git a/src/Ookii.CommandLine/ShortAliasAttribute.cs b/src/Ookii.CommandLine/ShortAliasAttribute.cs index 38fcc79b..23ba9383 100644 --- a/src/Ookii.CommandLine/ShortAliasAttribute.cs +++ b/src/Ookii.CommandLine/ShortAliasAttribute.cs @@ -10,10 +10,10 @@ namespace Ookii.CommandLine; /// 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 -/// . +/// 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 @@ -29,6 +29,7 @@ namespace Ookii.CommandLine; /// property to to exclude them. /// /// +/// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)] public sealed class ShortAliasAttribute : Attribute { diff --git a/src/Ookii.CommandLine/Terminal/TextFormat.cs b/src/Ookii.CommandLine/Terminal/TextFormat.cs index b554d3ad..5d5c4b64 100644 --- a/src/Ookii.CommandLine/Terminal/TextFormat.cs +++ b/src/Ookii.CommandLine/Terminal/TextFormat.cs @@ -250,7 +250,7 @@ public override bool Equals(object? obj) public override int GetHashCode() => Value.GetHashCode(); /// - /// Gets the text formatting string/ + /// Gets the text formatting string. /// /// /// A string containing virtual terminal sequences, or an empty string if this structure was diff --git a/src/Ookii.CommandLine/UsageWriter.cs b/src/Ookii.CommandLine/UsageWriter.cs index efe45fad..02e4e6cb 100644 --- a/src/Ookii.CommandLine/UsageWriter.cs +++ b/src/Ookii.CommandLine/UsageWriter.cs @@ -12,7 +12,7 @@ namespace Ookii.CommandLine; /// -/// Creates usage help for the class and the +/// Creates usage help for the class and the /// class. /// /// @@ -31,6 +31,7 @@ namespace Ookii.CommandLine; /// these properties. /// /// +/// public class UsageWriter { #region Nested types @@ -67,8 +68,7 @@ protected enum Operation public const int DefaultSyntaxIndent = 3; /// - /// The default indentation for the argument descriptions for the - /// mode. + /// The default indentation for the argument descriptions. /// /// /// The default indentation, which is eight characters. @@ -161,13 +161,18 @@ public UsageWriter(LineWrappingTextWriter? writer = null, bool? useColor = null) public int ApplicationDescriptionIndent { get; set; } = DefaultApplicationDescriptionIndent; /// - /// Gets or sets a value that overrides the default application executable name used in the - /// usage syntax. + /// Gets or sets the application executable name used in the usage help. /// /// - /// The application executable name, or to use the default value, - /// determined by calling . + /// 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] @@ -231,7 +236,8 @@ public bool IncludeExecutableExtension 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. + /// 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 @@ -245,8 +251,8 @@ public bool IncludeExecutableExtension /// length. /// /// - /// This value is not used if the maximum line length of the to which the usage - /// is being written is less than 30. + /// This value is used by the base implementation of the + /// class, unless the property is . /// /// public int SyntaxIndent { get; set; } = DefaultSyntaxIndent; @@ -257,13 +263,12 @@ public bool IncludeExecutableExtension /// /// /// to use short names for arguments that have one; otherwise, - /// to use an empty string. The default value is - /// . + /// to use the long name. The default value is . /// /// /// - /// This property is only used when the property is - /// . + /// This property is only used when the + /// property is . /// /// public bool UseShortNamesForSyntax { get; set; } @@ -301,11 +306,8 @@ public bool IncludeExecutableExtension /// /// /// - /// 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. + /// This value is used by the base implementation of the + /// method, unless the property is . /// /// public int ArgumentDescriptionIndent { get; set; } = DefaultArgumentDescriptionIndent; @@ -354,23 +356,28 @@ public bool IncludeExecutableExtension public TextFormat ArgumentDescriptionColor { get; set; } = TextFormat.ForegroundGreen; /// - /// Gets or sets a value indicating whether white space, rather than the first item of the - /// property, is used to separate - /// arguments and their values in the command line syntax. + /// 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 a colon. - /// The default value is . + /// 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. + /// 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 true. + /// 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; @@ -400,11 +407,16 @@ public bool IncludeExecutableExtension /// /// 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 + /// Gets or sets a value indicating whether the /// attributes of an argument should be included in the argument description. /// /// @@ -416,12 +428,17 @@ public bool IncludeExecutableExtension /// 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 lines between arguments in the description list. + /// method will write a blank line between arguments in the description list. /// /// /// to write a blank line; otherwise, . The @@ -430,7 +447,8 @@ public bool IncludeExecutableExtension public bool BlankLineAfterDescription { get; set; } = true; /// - /// Gets or sets the sequence used to reset color applied a usage help element. + /// 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 @@ -456,6 +474,10 @@ public bool IncludeExecutableExtension /// 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; } @@ -484,8 +506,8 @@ public bool IncludeExecutableExtension /// 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. + /// 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; @@ -498,15 +520,15 @@ public bool IncludeExecutableExtension /// /// /// - /// This value is used by the base implementation of the - /// class, unless the property is . + /// 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 lines between commands in the command list. + /// method will write a blank line between commands in the command list. /// /// /// to write a blank line; otherwise, . The @@ -526,23 +548,48 @@ public bool IncludeExecutableExtension /// /// /// 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; and - /// 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. + /// 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; } @@ -566,6 +613,10 @@ public bool IncludeExecutableExtension /// 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; } @@ -664,7 +715,8 @@ protected Operation OperationInProgress protected virtual bool ShouldIndent => Writer.MaximumLineLength is 0 or >= MinimumLineWidthForIndent; /// - /// Gets the separator used for argument names, command names, and aliases. + /// Gets the separator used between multiple consecutive argument names, command names, and + /// aliases in the usage help. /// /// /// The string ", ". @@ -759,7 +811,7 @@ public string GetUsage(CommandLineParser parser, UsageHelpRequest request = Usag /// Returns a string with usage help for the specified command manager. /// /// A string containing the usage help. - /// The + /// The . /// /// The length at which to white-space wrap lines in the output, or 0 to disable wrapping. /// @@ -878,11 +930,6 @@ protected virtual void WriteParserUsageSyntax() WriteUsageSyntaxPrefix(); foreach (CommandLineArgument argument in GetArgumentsInUsageOrder()) { - if (argument.IsHidden) - { - continue; - } - Write(" "); if (UseAbbreviatedSyntax && argument.Position == null) { @@ -919,8 +966,11 @@ protected virtual void WriteParserUsageSyntax() /// 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; + protected virtual IEnumerable GetArgumentsInUsageOrder() => Parser.Arguments.Where(a => !a.IsHidden); /// /// Write the prefix for the usage syntax, including the executable name and, for @@ -928,14 +978,14 @@ protected virtual void WriteParserUsageSyntax() /// /// /// - /// 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. + /// 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's not . + /// property if it is not . /// /// /// This method is called by the base implementation of the @@ -1302,7 +1352,8 @@ protected virtual void WriteArgumentDescription(CommandLineArgument argument) /// If color is enabled, the property is used. /// /// - /// This method is called by the base implementation of . + /// This method is called by the base implementation of the + /// method. /// /// protected virtual void WriteArgumentDescriptionHeader(CommandLineArgument argument) @@ -1425,7 +1476,7 @@ protected virtual void WriteArgumentNameForDescription(string argumentName, stri /// For example, "<String>". /// /// - /// This method is called by the base implementation of the + /// This method is called by the base implementation of the /// method and by the method.. /// /// @@ -1440,10 +1491,10 @@ protected virtual void WriteValueDescriptionForDescription(string valueDescripti /// /// /// The default implementation surrounds the value written by the - /// method with angle brackets, to indicate that it is optional. + /// method with square brackets, to indicate that it is optional. /// /// - /// This method is called by the base implementation of the + /// This method is called by the base implementation of the /// method for switch arguments. /// /// @@ -1530,7 +1581,7 @@ protected virtual void WriteAlias(string alias, string prefix) /// The base implementation just writes the description text. /// /// - /// This method is called by the base implementation of the + /// This method is called by the base implementation of the /// method. /// /// @@ -1550,7 +1601,7 @@ protected virtual void WriteArgumentDescription(string description) /// space. /// /// - /// This method is called by the base implementation of the + /// This method is called by the base implementation of the /// method if the property is /// . /// @@ -1578,7 +1629,7 @@ protected virtual void WriteArgumentValidators(CommandLineArgument argument) /// leading space. /// /// - /// This method is called by the base implementation of the + /// This method is called by the base implementation of the /// method if the property is /// and the property /// is not . @@ -1898,7 +1949,7 @@ protected virtual void WriteCommandAliases(IEnumerable aliases) /// The base implementation just writes the description text. /// /// - /// This method is called by the base implementation of the + /// This method is called by the base implementation of the /// method. /// /// @@ -1947,9 +1998,9 @@ protected virtual void WriteSpacing(int count) /// 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. + /// 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); @@ -1960,8 +2011,8 @@ protected virtual void WriteSpacing(int count) /// 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, + /// 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. /// /// @@ -1987,14 +2038,9 @@ protected virtual void WriteSpacing(int count) /// /// Writes a string with virtual terminal sequences only if color is enabled. /// - /// The string containing the color formatting. + /// 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 . /// /// @@ -2007,7 +2053,7 @@ protected void WriteColor(TextFormat color) } /// - /// Returns the color to the previous value, if color is enabled. + /// Returns the output color to the value before modifications, if color is enabled. /// /// /// diff --git a/src/Ookii.CommandLine/ValueDescriptionAttribute.cs b/src/Ookii.CommandLine/ValueDescriptionAttribute.cs index 5d99a2b8..95b9ec15 100644 --- a/src/Ookii.CommandLine/ValueDescriptionAttribute.cs +++ b/src/Ookii.CommandLine/ValueDescriptionAttribute.cs @@ -6,12 +6,6 @@ namespace Ookii.CommandLine; /// /// Supplies a short description of the arguments'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 @@ -30,7 +24,7 @@ namespace Ookii.CommandLine; /// use the property. /// /// -/// The value description is used only when generating usage help. For example, the usage for an +/// 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>". /// /// @@ -49,8 +43,10 @@ public class ValueDescriptionAttribute : Attribute /// /// 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)); From dd51da1ee9a476fa8c8c48d51ae07632cb20f515 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 26 Jun 2023 18:24:40 -0700 Subject: [PATCH 192/234] XML comment updates for the Commands namespace. --- .../Commands/AsyncCommandBase.cs | 9 +- .../Commands/CommandAttribute.cs | 30 ++--- src/Ookii.CommandLine/Commands/CommandInfo.cs | 105 ++++++++++++++---- .../Commands/IAsyncCommand.cs | 7 +- 4 files changed, 113 insertions(+), 38 deletions(-) diff --git a/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs b/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs index c0f4bd85..89aa9f5c 100644 --- a/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs +++ b/src/Ookii.CommandLine/Commands/AsyncCommandBase.cs @@ -3,9 +3,16 @@ namespace Ookii.CommandLine.Commands; /// -/// Base class for asynchronous tasks that want the method to +/// 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 { /// diff --git a/src/Ookii.CommandLine/Commands/CommandAttribute.cs b/src/Ookii.CommandLine/Commands/CommandAttribute.cs index 8d7fd5c9..58300004 100644 --- a/src/Ookii.CommandLine/Commands/CommandAttribute.cs +++ b/src/Ookii.CommandLine/Commands/CommandAttribute.cs @@ -9,19 +9,17 @@ namespace Ookii.CommandLine.Commands; /// /// /// To be considered a subcommand, a class must both implement the -/// interface and have the applied. +/// 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. /// /// -/// 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 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. /// /// -/// If a command has no 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 +/// Alternative names for a command can be given by using the /// attribute. /// /// @@ -37,7 +35,7 @@ public sealed class CommandAttribute : Attribute /// /// /// - /// If a command has no explicit name, its name is determined by taking the type name + /// 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. /// @@ -49,7 +47,10 @@ 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. + /// + /// The name of the command, which can be used to invoke the command or to retrieve it using the + /// method. + /// /// /// is . /// @@ -59,11 +60,12 @@ public CommandAttribute(string commandName) } /// - /// Gets the name of the command, which can be used to locate it using the method. + /// 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 to use the type name as the command - /// name. + /// The name of the command, or if the target type name will be used as + /// the name. /// public string? CommandName => _commandName; diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index 2e9153f2..a39606e0 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -13,6 +13,7 @@ namespace Ookii.CommandLine.Commands; /// /// /// +/// public abstract class CommandInfo { private readonly CommandManager _manager; @@ -32,9 +33,6 @@ public abstract class CommandInfo /// /// or is . /// - /// - /// is not a command type. - /// protected CommandInfo(Type commandType, CommandAttribute attribute, CommandManager manager, Type? parentCommandType) { _manager = manager ?? throw new ArgumentNullException(nameof(manager)); @@ -126,8 +124,8 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// /// /// - /// Aliases for a command are specified by using the on a - /// class implementing the interface. + /// Aliases for a command are specified by using the attribute + /// on a class implementing the interface. /// /// public abstract IEnumerable Aliases { get; } @@ -152,7 +150,7 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) public Type? ParentCommandType { get; } /// - /// Creates an instance of the command type. + /// Creates an instance of the command type parsing the specified arguments. /// /// The arguments to the command. /// The index in at which to start parsing the arguments. @@ -160,18 +158,51 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// 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. + /// + /// /// - /// is . + /// is . + /// + /// + /// does not fall inside the bounds of . /// - /// does not fall inside the bounds of . public ICommand? CreateInstance(string[] args, int index) + => CreateInstance(args.AsMemory(index)); + + /// + /// 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, index); + var (command, _) = CreateInstanceWithResult(args); return command; } + /// - /// Creates an instance of the command type. + /// 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. /// The index in at which to start parsing the arguments. @@ -181,6 +212,13 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// /// /// + /// 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. /// @@ -205,7 +243,8 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) } /// - /// Creates an instance of the command type. + /// 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. /// @@ -214,6 +253,13 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// /// /// + /// 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. /// @@ -235,7 +281,8 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) } /// - /// Creates a instance that can be used to instantiate + /// Creates a instance for the type indicated by the + /// property. /// /// /// A instance for the . @@ -246,8 +293,9 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// /// /// If the property is , the - /// command cannot be created suing the class, and you - /// must use the method. + /// command cannot be created using the class, and you + /// must use the method or + /// method instead. /// /// public abstract CommandLineParser CreateParser(); @@ -260,6 +308,12 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// /// 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(); /// @@ -270,6 +324,13 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// 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 . /// @@ -293,7 +354,7 @@ public bool MatchesName(string name) /// /// The prefix to check for. /// - /// if the is a prefix of the + /// if is a prefix of the /// property or any of the items in the property. /// /// @@ -326,8 +387,10 @@ public bool MatchesPrefix(string prefix) /// or is . /// /// - /// A class with information about the command, or - /// if was not a command. + /// 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)] @@ -347,7 +410,8 @@ public bool MatchesPrefix(string prefix) /// or is . /// /// - /// is not a command. + /// is does not implement the interface, + /// does not have the attribute, or is . /// /// /// A class with information about the command. @@ -363,8 +427,9 @@ public static CommandInfo Create(Type commandType, CommandManager manager) /// /// The type that implements the subcommand. /// - /// if the type implements the interface and - /// has the applied; otherwise, . + /// if the type implements the interface, has the + /// attribute applied, and is not ; + /// otherwise, . /// /// /// is . diff --git a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs index f4880213..9826c3f0 100644 --- a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs +++ b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs @@ -23,14 +23,15 @@ public interface IAsyncCommand : ICommand /// /// /// - /// Typically, your applications Main() method should return the exit code of the + /// 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 task. Use the - /// class for a default implementation that does this. + /// method to invoke this method and wait for + /// it. Use the class for a default implementation that does + /// this. /// /// Task RunAsync(); From 1f48a90ececc43fe790d2f77fb4c4324e7913b51 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Mon, 26 Jun 2023 18:28:16 -0700 Subject: [PATCH 193/234] Improved trimming annotation for IsCommand. --- src/Ookii.CommandLine/Commands/CommandInfo.cs | 26 +++++++++++++++++-- .../Support/ReflectionCommandInfo.cs | 15 ----------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index a39606e0..6807fefb 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; namespace Ookii.CommandLine.Commands; @@ -434,14 +435,35 @@ public static CommandInfo Create(Type commandType, CommandManager manager) /// /// is . /// + public static bool IsCommand( #if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Command information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute and GeneratedCommandManagerAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] #endif - public static bool IsCommand(Type commandType) => ReflectionCommandInfo.GetCommandAttribute(commandType) != null; + Type commandType + ) => GetCommandAttribute(commandType) != null; 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) + { + throw new ArgumentNullException(nameof(commandType)); + } + + if (commandType.IsAbstract || !commandType.ImplementsInterface(typeof(ICommand))) + { + return null; + } + + return commandType.GetCustomAttribute(); + } + private static string GetName(CommandAttribute attribute, Type commandType, CommandOptions? options) { return attribute.CommandName ?? diff --git a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs index 7dcdd5b2..98e61033 100644 --- a/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/ReflectionCommandInfo.cs @@ -60,21 +60,6 @@ public override ICommandWithCustomParsing CreateInstanceWithCustomParsing() return (ICommandWithCustomParsing)Activator.CreateInstance(CommandType)!; } - internal static CommandAttribute? GetCommandAttribute(Type commandType) - { - if (commandType == null) - { - throw new ArgumentNullException(nameof(commandType)); - } - - if (commandType.IsAbstract || !commandType.ImplementsInterface(typeof(ICommand))) - { - return null; - } - - return commandType.GetCustomAttribute(); - } - private static CommandAttribute GetCommandAttributeOrThrow(Type commandType) { return GetCommandAttribute(commandType) ?? From 5084898d4e09b7aa8120d87e0bd8615d701b2cae Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 27 Jun 2023 16:16:03 -0700 Subject: [PATCH 194/234] More XML comment updates for the Commands namespace. --- src/Ookii.CommandLine/CommandLineParser.cs | 1 + .../Commands/CommandManager.cs | 379 +++++++++++++----- .../Commands/CommandOptions.cs | 51 ++- .../GeneratedCommandManagerAttribute.cs | 22 +- .../Commands/IAsyncCommand.cs | 4 + src/Ookii.CommandLine/Commands/ICommand.cs | 2 +- .../Commands/ICommandWithCustomParsing.cs | 6 +- .../Commands/ParentCommand.cs | 62 ++- .../Commands/ParentCommandAttribute.cs | 4 +- 9 files changed, 370 insertions(+), 161 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 3dc1be37..e73e7eaf 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -343,6 +343,7 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) /// parsing behavior. See the property for details. /// /// + /// public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null) { _provider = provider ?? throw new ArgumentNullException(nameof(provider)); diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index fc22c73f..274bc9f7 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -31,11 +31,12 @@ namespace Ookii.CommandLine.Commands; /// the method to implement the command's functionality. /// /// -/// Subcommands classes are instantiated using the class, and -/// follow the same rules as command line arguments classes. +/// 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 multiple assemblies. +/// Commands can be defined in a single assembly, or in multiple assemblies. /// /// /// If you reuse the same instance or @@ -45,6 +46,7 @@ namespace Ookii.CommandLine.Commands; /// /// /// Usage documentation +/// public class CommandManager { private readonly CommandProvider _provider; @@ -60,7 +62,14 @@ public class CommandManager /// /// /// - /// Both public and internal command classes will be used. + /// The class will look in the calling assembly for any public + /// or internal classes that implement the interface, have the + /// attribute, and are not . + /// + /// + /// 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 @@ -89,6 +98,19 @@ public CommandManager(CommandOptions? options = null) /// 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)); @@ -96,7 +118,8 @@ protected CommandManager(CommandProvider provider, CommandOptions? options = nul } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class using the specified + /// assembly. /// /// The assembly containing the commands. /// @@ -108,10 +131,20 @@ protected CommandManager(CommandProvider provider, CommandOptions? options = nul /// /// /// + /// The class will look in the specified assembly for any public + /// classes that implement the interface, have the + /// attribute, and are not . + /// + /// /// If is the assembly that called this constructor, both public /// and internal command classes will be used. Otherwise, only public command 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 @@ -128,7 +161,8 @@ public CommandManager(Assembly assembly, CommandOptions? options = null) } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class using the specified + /// assemblies. /// /// The assemblies containing the commands. /// @@ -140,9 +174,19 @@ public CommandManager(Assembly assembly, CommandOptions? options = null) /// /// /// + /// 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. Otherwise, only public - /// command classes are used for that assembly. + /// 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 @@ -188,10 +232,11 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// /// /// - /// 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 + /// 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 /// . /// /// @@ -201,12 +246,12 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// Gets the kind of used to supply the commands. /// /// - /// One of the values of the enumeration. + /// One of the values of the enumeration. /// public ProviderKind ProviderKind => _provider.Kind; /// - /// Gets information about the commands. + /// Gets information about all the commands managed by this instance. /// /// /// Information about every subcommand defined in the assemblies, ordered by command name. @@ -217,15 +262,18 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// predicate are not returned. /// /// - /// If the 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. + /// 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 added if the - /// property is and there is no command with a conflicting name. + /// 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() @@ -259,8 +307,8 @@ public IEnumerable GetCommands() /// /// /// 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. + /// 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 , @@ -281,16 +329,18 @@ public IEnumerable GetCommands() /// predicate are not returned. /// /// - /// If the 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. + /// 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 matches the - /// name of the automatic version command, and not any other command name. + /// 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) @@ -351,37 +401,43 @@ public IEnumerable GetCommands() /// The name of the command. /// The arguments to the command. /// - /// An instance a class implement the interface, or - /// if the command was not found or an error occurred parsing the arguments. + /// 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 - /// . If an error occurs parsing the command's arguments, - /// the error message is written to , and the - /// command's usage information is written to . + /// 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 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 . + /// 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 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. + /// 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. + /// 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) @@ -414,11 +470,17 @@ public IEnumerable GetCommands() /// The name of the command. /// The arguments to the command. /// The index in at which to start parsing the arguments. + /// + /// is . + /// + /// + /// does not fall within the bounds of . + /// public ICommand? CreateCommand(string? commandName, string[] args, int index) { if (args == null) { - throw new ArgumentNullException(nameof(index)); + throw new ArgumentNullException(nameof(args)); } if (index < 0 || index > args.Length) @@ -434,6 +496,19 @@ public IEnumerable GetCommands() /// 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 (starting at ) + /// is the command name and the remaining ones are arguments for the command. + /// + /// + /// The index in at which to start parsing the arguments. + /// + /// + /// is . + /// + /// + /// does not fall within the bounds of . + /// public ICommand? CreateCommand(string[] args, int index = 0) { if (args == null) @@ -455,6 +530,10 @@ public IEnumerable GetCommands() /// 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; @@ -468,8 +547,9 @@ public IEnumerable GetCommands() } /// - /// 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. + /// 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. /// /// /// @@ -503,8 +583,8 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the , - /// method and then invokes the method on 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 @@ -536,8 +616,8 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the , - /// method and then invokes the method on 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 @@ -562,10 +642,14 @@ public IEnumerable GetCommands() /// 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. + /// 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 @@ -590,10 +674,17 @@ public IEnumerable GetCommands() /// 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 (starting at ) + /// is the command name and the remaining ones are arguments for the command. + /// + /// + /// The index in at which to start parsing the arguments. + /// /// /// - /// This function creates the command by invoking the , - /// method and then invokes the method on 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 @@ -623,19 +714,26 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the , - /// method and then invokes the method on 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. + /// 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() @@ -656,7 +754,7 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the , + /// 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. @@ -666,11 +764,18 @@ public IEnumerable GetCommands() /// 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. + /// 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) @@ -696,7 +801,7 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the , + /// 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. @@ -706,11 +811,18 @@ public IEnumerable GetCommands() /// 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. + /// 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, int index) @@ -726,12 +838,16 @@ public IEnumerable GetCommands() /// /// - /// Finds and instantiates the subcommand with the specified name, and if it succeeds, - /// runs it asynchronously. If it fails, writes error and usage information. + /// 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 , + /// 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. @@ -741,11 +857,18 @@ public IEnumerable GetCommands() /// 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. + /// 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) @@ -761,12 +884,19 @@ public IEnumerable GetCommands() /// /// - /// Finds and instantiates the subcommand with the specified name, and if it succeeds, - /// runs it asynchronously. If it fails, writes error and usage information. + /// 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 (starting at ) + /// is the command name and the remaining ones are arguments for the command. + /// + /// + /// The index in at which to start parsing the arguments. + /// /// /// - /// This function creates the command by invoking the , + /// 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. @@ -776,11 +906,18 @@ public IEnumerable GetCommands() /// 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. + /// 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, int index = 0) @@ -794,15 +931,17 @@ public IEnumerable GetCommands() 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 , + /// 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. @@ -812,11 +951,18 @@ public IEnumerable GetCommands() /// 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. + /// 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() @@ -836,7 +982,8 @@ public IEnumerable GetCommands() /// /// /// This method writes usage help for the application, including a list of all - /// subcommand names and their descriptions to . + /// subcommand names and their descriptions using the + /// property. /// /// /// A command's name is retrieved from its attribute, @@ -847,11 +994,18 @@ public IEnumerable GetCommands() /// 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. + /// 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() @@ -873,11 +1027,18 @@ public void WriteUsage() /// 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. + /// 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() diff --git a/src/Ookii.CommandLine/Commands/CommandOptions.cs b/src/Ookii.CommandLine/Commands/CommandOptions.cs index 7c69c931..41b1753b 100644 --- a/src/Ookii.CommandLine/Commands/CommandOptions.cs +++ b/src/Ookii.CommandLine/Commands/CommandOptions.cs @@ -5,6 +5,7 @@ namespace Ookii.CommandLine.Commands; /// /// Provides options for the class. /// +/// public class CommandOptions : ParseOptions { /// @@ -19,8 +20,8 @@ public class CommandOptions : ParseOptions /// 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. "argument-name"). + /// 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 @@ -33,9 +34,9 @@ public class CommandOptions : ParseOptions /// /// /// This property will only return if the above properties are the - /// indicated values, except that and - /// can be any case-sensitive comparison. It will - /// return for any other combination of values, not just the ones + /// 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. /// /// @@ -68,7 +69,7 @@ public override bool IsPosix } /// - /// Gets or set the type of string comparison to use for argument names. + /// Gets or sets the type of string comparison to use for argument names. /// /// /// One of the values of the enumeration. The default value @@ -120,13 +121,13 @@ public override bool IsPosix /// 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. + /// 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. + /// The value of this property is case sensitive. /// /// public string? StripCommandNameSuffix { get; set; } = "Command"; @@ -140,13 +141,15 @@ public override bool IsPosix /// /// /// - /// 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. + /// 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. /// - /// - /// 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; } @@ -166,7 +169,7 @@ public override bool IsPosix /// /// /// All other commands are filtered out and will not be returned, created, or executed - /// by the command manager. + /// by the class. /// /// public Type? ParentCommand { get; set; } @@ -183,14 +186,22 @@ public override bool IsPosix /// /// 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 unique prefixes of a command name are + /// Gets or sets a value that indicates whether unique prefixes of a command name or alias are /// automatically used as aliases. /// /// @@ -202,7 +213,7 @@ public override bool IsPosix /// /// 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 argument. For example, given two commands "read" + /// 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. diff --git a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs index 14fcb58b..6772967b 100644 --- a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs +++ b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs @@ -8,17 +8,22 @@ namespace Ookii.CommandLine.Commands; /// /// /// -/// When using this attribute, source generation is used to find and instantiate subcommand -/// classes in the current assembly, 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. +/// 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 the command class. +/// attribute on each command class. /// /// +/// /// Source generation [AttributeUsage(AttributeTargets.Class)] public sealed class GeneratedCommandManagerAttribute : Attribute @@ -34,8 +39,9 @@ public sealed class GeneratedCommandManagerAttribute : Attribute /// /// /// The assemblies used must be directly referenced by your project. Dynamically loading - /// assemblies is not supported by this method; use the - /// constructor instead. + /// 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 diff --git a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs index 9826c3f0..51150529 100644 --- a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs +++ b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs @@ -11,6 +11,10 @@ namespace Ookii.CommandLine.Commands; /// 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 { diff --git a/src/Ookii.CommandLine/Commands/ICommand.cs b/src/Ookii.CommandLine/Commands/ICommand.cs index fefc467f..e5562434 100644 --- a/src/Ookii.CommandLine/Commands/ICommand.cs +++ b/src/Ookii.CommandLine/Commands/ICommand.cs @@ -23,7 +23,7 @@ public interface ICommand /// The exit code for the command. /// /// - /// Typically, your applications Main() method should return the exit code of the + /// Typically, your application's Main() method should return the exit code of the /// command that was executed. /// /// diff --git a/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs b/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs index 8000b154..188af561 100644 --- a/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs +++ b/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs @@ -7,9 +7,9 @@ namespace Ookii.CommandLine.Commands; /// /// /// -/// Unlike commands that only implement the interfaces, commands that +/// Unlike commands that only implement the interface, commands that /// implement the interface are not created with the -/// . Instead, they must have a public constructor with no +/// class. Instead, they must have a public constructor with no /// parameters, and must parse the arguments manually by implementing the /// method. /// @@ -20,7 +20,7 @@ public interface ICommandWithCustomParsing : ICommand /// /// Parses the arguments for the command. /// - /// The arguments. + /// 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 index 20642168..890e3f60 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommand.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommand.cs @@ -8,15 +8,15 @@ namespace Ookii.CommandLine.Commands; ///
/// /// -/// The , along with the class, -/// 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. +/// 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. +/// 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 @@ -24,7 +24,7 @@ namespace Ookii.CommandLine.Commands; /// /// /// The class is based on the -/// attribute, so derived classes cannot define any arguments or use other functionality that +/// interface, so derived classes cannot define any arguments or use other functionality that /// depends on the class. /// /// @@ -34,15 +34,23 @@ public abstract class ParentCommand : ICommandWithCustomParsing, IAsyncCommand private ICommand? _childCommand; /// - /// Gets the exit code to return if parsing command line arguments for a nested subcommand - /// failed. + /// 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); @@ -84,7 +92,7 @@ public void Parse(ReadOnlyMemory args, CommandManager manager) { handler = (sender, e) => { - OnDuplicateArgumentWarning(e.Argument, e.NewValue); + e.KeepOldValue = !OnDuplicateArgumentWarning(e.Argument, e.NewValue); }; parser.DuplicateArgument += handler; @@ -107,7 +115,13 @@ public void Parse(ReadOnlyMemory args, CommandManager manager) } } - /// + /// + /// 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) @@ -118,7 +132,14 @@ public virtual int Run() 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) @@ -173,6 +194,10 @@ protected virtual void OnChildCommandNotFound(string? commandName, CommandManage ///
/// 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 @@ -183,18 +208,19 @@ protected virtual void OnChildCommandNotFound(string? commandName, CommandManage /// interface. /// /// - protected virtual void OnDuplicateArgumentWarning(CommandLineArgument argument, string? newValue) + 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 both success, cancellation, and failure. + /// Function called after parsing, on success, cancellation, and failure. /// /// - /// The for the nested subcommand, or + /// The instance for the nested subcommand, or /// if the nested subcommand used the interface. /// /// @@ -204,7 +230,7 @@ protected virtual void OnDuplicateArgumentWarning(CommandLineArgument argument, /// /// /// 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 + /// subcommand if applicable. On success, or for nested subcommands using the /// interface, it does nothing. /// /// diff --git a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs index e67673ed..9763ef82 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs @@ -8,8 +8,8 @@ namespace Ookii.CommandLine.Commands; ///
/// /// -/// If you wish to have a command with nested subcommands, apply this attribute to the nested -/// subcommand classes. The class will only return commands whose +/// 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. /// From 251aa79fac8b5501e5ee27f222433d147ca5f7bb Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 27 Jun 2023 16:16:03 -0700 Subject: [PATCH 195/234] More XML comment updates for the Commands namespace. --- src/Ookii.CommandLine/CommandLineParser.cs | 1 + .../Commands/CommandManager.cs | 379 +++++++++++++----- .../Commands/CommandOptions.cs | 51 ++- .../GeneratedCommandManagerAttribute.cs | 22 +- .../Commands/IAsyncCommand.cs | 4 + src/Ookii.CommandLine/Commands/ICommand.cs | 2 +- .../Commands/ICommandWithCustomParsing.cs | 6 +- .../Commands/ParentCommand.cs | 62 ++- .../Commands/ParentCommandAttribute.cs | 6 +- 9 files changed, 371 insertions(+), 162 deletions(-) diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index 3dc1be37..e73e7eaf 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -343,6 +343,7 @@ public CommandLineParser(Type argumentsType, ParseOptions? options = null) /// parsing behavior. See the property for details. /// /// + /// public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null) { _provider = provider ?? throw new ArgumentNullException(nameof(provider)); diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index fc22c73f..274bc9f7 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -31,11 +31,12 @@ namespace Ookii.CommandLine.Commands; /// the method to implement the command's functionality. /// /// -/// Subcommands classes are instantiated using the class, and -/// follow the same rules as command line arguments classes. +/// 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 multiple assemblies. +/// Commands can be defined in a single assembly, or in multiple assemblies. /// /// /// If you reuse the same instance or @@ -45,6 +46,7 @@ namespace Ookii.CommandLine.Commands; /// /// /// Usage documentation +/// public class CommandManager { private readonly CommandProvider _provider; @@ -60,7 +62,14 @@ public class CommandManager /// /// /// - /// Both public and internal command classes will be used. + /// The class will look in the calling assembly for any public + /// or internal classes that implement the interface, have the + /// attribute, and are not . + /// + /// + /// 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 @@ -89,6 +98,19 @@ public CommandManager(CommandOptions? options = null) /// 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)); @@ -96,7 +118,8 @@ protected CommandManager(CommandProvider provider, CommandOptions? options = nul } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class using the specified + /// assembly. /// /// The assembly containing the commands. /// @@ -108,10 +131,20 @@ protected CommandManager(CommandProvider provider, CommandOptions? options = nul /// /// /// + /// The class will look in the specified assembly for any public + /// classes that implement the interface, have the + /// attribute, and are not . + /// + /// /// If is the assembly that called this constructor, both public /// and internal command classes will be used. Otherwise, only public command 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 @@ -128,7 +161,8 @@ public CommandManager(Assembly assembly, CommandOptions? options = null) } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class using the specified + /// assemblies. /// /// The assemblies containing the commands. /// @@ -140,9 +174,19 @@ public CommandManager(Assembly assembly, CommandOptions? options = null) /// /// /// + /// 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. Otherwise, only public - /// command classes are used for that assembly. + /// 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 @@ -188,10 +232,11 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// /// /// - /// 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 + /// 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 /// . /// /// @@ -201,12 +246,12 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// Gets the kind of used to supply the commands. ///
/// - /// One of the values of the enumeration. + /// One of the values of the enumeration. /// public ProviderKind ProviderKind => _provider.Kind; /// - /// Gets information about the commands. + /// Gets information about all the commands managed by this instance. /// /// /// Information about every subcommand defined in the assemblies, ordered by command name. @@ -217,15 +262,18 @@ public CommandManager(IEnumerable assemblies, CommandOptions? options /// predicate are not returned. /// /// - /// If the 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. + /// 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 added if the - /// property is and there is no command with a conflicting name. + /// 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() @@ -259,8 +307,8 @@ public IEnumerable GetCommands() /// /// /// 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. + /// 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 , @@ -281,16 +329,18 @@ public IEnumerable GetCommands() /// predicate are not returned. /// /// - /// If the 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. + /// 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 matches the - /// name of the automatic version command, and not any other command name. + /// 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) @@ -351,37 +401,43 @@ public IEnumerable GetCommands() /// The name of the command. /// The arguments to the command. /// - /// An instance a class implement the interface, or - /// if the command was not found or an error occurred parsing the arguments. + /// 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 - /// . If an error occurs parsing the command's arguments, - /// the error message is written to , and the - /// command's usage information is written to . + /// 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 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 . + /// 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 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. + /// 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. + /// 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) @@ -414,11 +470,17 @@ public IEnumerable GetCommands() /// The name of the command. /// The arguments to the command. /// The index in at which to start parsing the arguments. + /// + /// is . + /// + /// + /// does not fall within the bounds of . + /// public ICommand? CreateCommand(string? commandName, string[] args, int index) { if (args == null) { - throw new ArgumentNullException(nameof(index)); + throw new ArgumentNullException(nameof(args)); } if (index < 0 || index > args.Length) @@ -434,6 +496,19 @@ public IEnumerable GetCommands() /// 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 (starting at ) + /// is the command name and the remaining ones are arguments for the command. + /// + /// + /// The index in at which to start parsing the arguments. + /// + /// + /// is . + /// + /// + /// does not fall within the bounds of . + /// public ICommand? CreateCommand(string[] args, int index = 0) { if (args == null) @@ -455,6 +530,10 @@ public IEnumerable GetCommands() /// 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; @@ -468,8 +547,9 @@ public IEnumerable GetCommands() } /// - /// 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. + /// 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. /// /// /// @@ -503,8 +583,8 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the , - /// method and then invokes the method on 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 @@ -536,8 +616,8 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the , - /// method and then invokes the method on 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 @@ -562,10 +642,14 @@ public IEnumerable GetCommands() /// 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. + /// 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 @@ -590,10 +674,17 @@ public IEnumerable GetCommands() /// 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 (starting at ) + /// is the command name and the remaining ones are arguments for the command. + /// + /// + /// The index in at which to start parsing the arguments. + /// /// /// - /// This function creates the command by invoking the , - /// method and then invokes the method on 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 @@ -623,19 +714,26 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the , - /// method and then invokes the method on 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. + /// 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() @@ -656,7 +754,7 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the , + /// 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. @@ -666,11 +764,18 @@ public IEnumerable GetCommands() /// 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. + /// 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) @@ -696,7 +801,7 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the , + /// 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. @@ -706,11 +811,18 @@ public IEnumerable GetCommands() /// 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. + /// 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, int index) @@ -726,12 +838,16 @@ public IEnumerable GetCommands() /// /// - /// Finds and instantiates the subcommand with the specified name, and if it succeeds, - /// runs it asynchronously. If it fails, writes error and usage information. + /// 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 , + /// 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. @@ -741,11 +857,18 @@ public IEnumerable GetCommands() /// 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. + /// 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) @@ -761,12 +884,19 @@ public IEnumerable GetCommands() /// /// - /// Finds and instantiates the subcommand with the specified name, and if it succeeds, - /// runs it asynchronously. If it fails, writes error and usage information. + /// 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 (starting at ) + /// is the command name and the remaining ones are arguments for the command. + /// + /// + /// The index in at which to start parsing the arguments. + /// /// /// - /// This function creates the command by invoking the , + /// 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. @@ -776,11 +906,18 @@ public IEnumerable GetCommands() /// 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. + /// 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, int index = 0) @@ -794,15 +931,17 @@ public IEnumerable GetCommands() 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 , + /// 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. @@ -812,11 +951,18 @@ public IEnumerable GetCommands() /// 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. + /// 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() @@ -836,7 +982,8 @@ public IEnumerable GetCommands() /// /// /// This method writes usage help for the application, including a list of all - /// subcommand names and their descriptions to . + /// subcommand names and their descriptions using the + /// property. /// /// /// A command's name is retrieved from its attribute, @@ -847,11 +994,18 @@ public IEnumerable GetCommands() /// 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. + /// 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() @@ -873,11 +1027,18 @@ public void WriteUsage() /// 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. + /// 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() diff --git a/src/Ookii.CommandLine/Commands/CommandOptions.cs b/src/Ookii.CommandLine/Commands/CommandOptions.cs index 7c69c931..41b1753b 100644 --- a/src/Ookii.CommandLine/Commands/CommandOptions.cs +++ b/src/Ookii.CommandLine/Commands/CommandOptions.cs @@ -5,6 +5,7 @@ namespace Ookii.CommandLine.Commands; /// /// Provides options for the class. /// +/// public class CommandOptions : ParseOptions { /// @@ -19,8 +20,8 @@ public class CommandOptions : ParseOptions /// 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. "argument-name"). + /// 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 @@ -33,9 +34,9 @@ public class CommandOptions : ParseOptions /// /// /// This property will only return if the above properties are the - /// indicated values, except that and - /// can be any case-sensitive comparison. It will - /// return for any other combination of values, not just the ones + /// 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. /// /// @@ -68,7 +69,7 @@ public override bool IsPosix } /// - /// Gets or set the type of string comparison to use for argument names. + /// Gets or sets the type of string comparison to use for argument names. /// /// /// One of the values of the enumeration. The default value @@ -120,13 +121,13 @@ public override bool IsPosix /// 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. + /// 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. + /// The value of this property is case sensitive. /// /// public string? StripCommandNameSuffix { get; set; } = "Command"; @@ -140,13 +141,15 @@ public override bool IsPosix /// /// /// - /// 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. + /// 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. /// - /// - /// 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; } @@ -166,7 +169,7 @@ public override bool IsPosix /// /// /// All other commands are filtered out and will not be returned, created, or executed - /// by the command manager. + /// by the class. /// /// public Type? ParentCommand { get; set; } @@ -183,14 +186,22 @@ public override bool IsPosix /// /// 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 unique prefixes of a command name are + /// Gets or sets a value that indicates whether unique prefixes of a command name or alias are /// automatically used as aliases. /// /// @@ -202,7 +213,7 @@ public override bool IsPosix /// /// 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 argument. For example, given two commands "read" + /// 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. diff --git a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs index 14fcb58b..6772967b 100644 --- a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs +++ b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs @@ -8,17 +8,22 @@ namespace Ookii.CommandLine.Commands; /// /// /// -/// When using this attribute, source generation is used to find and instantiate subcommand -/// classes in the current assembly, 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. +/// 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 the command class. +/// attribute on each command class. /// /// +/// /// Source generation [AttributeUsage(AttributeTargets.Class)] public sealed class GeneratedCommandManagerAttribute : Attribute @@ -34,8 +39,9 @@ public sealed class GeneratedCommandManagerAttribute : Attribute /// /// /// The assemblies used must be directly referenced by your project. Dynamically loading - /// assemblies is not supported by this method; use the - /// constructor instead. + /// 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 diff --git a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs index 9826c3f0..51150529 100644 --- a/src/Ookii.CommandLine/Commands/IAsyncCommand.cs +++ b/src/Ookii.CommandLine/Commands/IAsyncCommand.cs @@ -11,6 +11,10 @@ namespace Ookii.CommandLine.Commands; /// 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 { diff --git a/src/Ookii.CommandLine/Commands/ICommand.cs b/src/Ookii.CommandLine/Commands/ICommand.cs index fefc467f..e5562434 100644 --- a/src/Ookii.CommandLine/Commands/ICommand.cs +++ b/src/Ookii.CommandLine/Commands/ICommand.cs @@ -23,7 +23,7 @@ public interface ICommand /// The exit code for the command. /// /// - /// Typically, your applications Main() method should return the exit code of the + /// Typically, your application's Main() method should return the exit code of the /// command that was executed. /// /// diff --git a/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs b/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs index 8000b154..188af561 100644 --- a/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs +++ b/src/Ookii.CommandLine/Commands/ICommandWithCustomParsing.cs @@ -7,9 +7,9 @@ namespace Ookii.CommandLine.Commands; ///
/// /// -/// Unlike commands that only implement the interfaces, commands that +/// Unlike commands that only implement the interface, commands that /// implement the interface are not created with the -/// . Instead, they must have a public constructor with no +/// class. Instead, they must have a public constructor with no /// parameters, and must parse the arguments manually by implementing the /// method. /// @@ -20,7 +20,7 @@ public interface ICommandWithCustomParsing : ICommand /// /// Parses the arguments for the command. /// - /// The arguments. + /// 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 index 20642168..890e3f60 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommand.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommand.cs @@ -8,15 +8,15 @@ namespace Ookii.CommandLine.Commands; ///
/// /// -/// The , along with the class, -/// 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. +/// 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. +/// 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 @@ -24,7 +24,7 @@ namespace Ookii.CommandLine.Commands; /// /// /// The class is based on the -/// attribute, so derived classes cannot define any arguments or use other functionality that +/// interface, so derived classes cannot define any arguments or use other functionality that /// depends on the class. /// /// @@ -34,15 +34,23 @@ public abstract class ParentCommand : ICommandWithCustomParsing, IAsyncCommand private ICommand? _childCommand; /// - /// Gets the exit code to return if parsing command line arguments for a nested subcommand - /// failed. + /// 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); @@ -84,7 +92,7 @@ public void Parse(ReadOnlyMemory args, CommandManager manager) { handler = (sender, e) => { - OnDuplicateArgumentWarning(e.Argument, e.NewValue); + e.KeepOldValue = !OnDuplicateArgumentWarning(e.Argument, e.NewValue); }; parser.DuplicateArgument += handler; @@ -107,7 +115,13 @@ public void Parse(ReadOnlyMemory args, CommandManager manager) } } - /// + /// + /// 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) @@ -118,7 +132,14 @@ public virtual int Run() 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) @@ -173,6 +194,10 @@ protected virtual void OnChildCommandNotFound(string? commandName, CommandManage ///
/// 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 @@ -183,18 +208,19 @@ protected virtual void OnChildCommandNotFound(string? commandName, CommandManage /// interface. /// /// - protected virtual void OnDuplicateArgumentWarning(CommandLineArgument argument, string? newValue) + 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 both success, cancellation, and failure. + /// Function called after parsing, on success, cancellation, and failure. /// /// - /// The for the nested subcommand, or + /// The instance for the nested subcommand, or /// if the nested subcommand used the interface. /// /// @@ -204,7 +230,7 @@ protected virtual void OnDuplicateArgumentWarning(CommandLineArgument argument, /// /// /// 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 + /// subcommand if applicable. On success, or for nested subcommands using the /// interface, it does nothing. /// /// diff --git a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs index e67673ed..ab28f1ec 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs @@ -8,8 +8,8 @@ namespace Ookii.CommandLine.Commands; ///
/// /// -/// If you wish to have a command with nested subcommands, apply this attribute to the nested -/// subcommand classes. The class will only return commands whose +/// 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. /// @@ -24,7 +24,7 @@ namespace Ookii.CommandLine.Commands; /// nested subcommands. /// /// -/// +/// [AttributeUsage(AttributeTargets.Class)] public sealed class ParentCommandAttribute : Attribute { From 1f25767af30586497115dfb0d6d4fea19052829a Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 27 Jun 2023 17:56:27 -0700 Subject: [PATCH 196/234] XML comment updates for the Conversion namespace. --- docs/Ookii.CommandLine.shfbproj | 2 +- .../Commands/ParentCommandAttribute.cs | 2 +- .../Conversion/ArgumentConverter.cs | 10 +- .../Conversion/ArgumentConverterAttribute.cs | 11 +- .../Conversion/BooleanConverter.cs | 46 ++++++- .../Conversion/EnumConverter.cs | 79 +++++++++-- .../GeneratedConverterNamespaceAttribute.cs | 6 +- .../Conversion/KeyConverterAttribute.cs | 15 +- .../Conversion/KeyValuePairConverter.cs | 130 +++++++++++++++--- .../Conversion/KeyValueSeparatorAttribute.cs | 4 + .../Conversion/NullableConverter.cs | 16 ++- .../Conversion/ParsableConverter.cs | 11 +- .../Conversion/SpanParsableConverter.cs | 11 +- .../Conversion/StringConverter.cs | 15 +- .../TypeConverterArgumentConverter.cs | 3 +- .../TypeConverterArgumentConverterGeneric.cs | 1 + .../Conversion/ValueConverterAttribute.cs | 11 +- .../Properties/Resources.Designer.cs | 9 ++ .../Properties/Resources.resx | 3 + 19 files changed, 326 insertions(+), 59 deletions(-) diff --git a/docs/Ookii.CommandLine.shfbproj b/docs/Ookii.CommandLine.shfbproj index b459dc27..297a8456 100644 --- a/docs/Ookii.CommandLine.shfbproj +++ b/docs/Ookii.CommandLine.shfbproj @@ -37,7 +37,7 @@ Provides attributes used to validate the value of arguments, and the relation between arguments. </para> <para> - Provides functionality for converting strings to the actual type of the argument. + 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. diff --git a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs index ab28f1ec..bdbc6c7c 100644 --- a/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs +++ b/src/Ookii.CommandLine/Commands/ParentCommandAttribute.cs @@ -38,7 +38,7 @@ public sealed class ParentCommandAttribute : Attribute /// /// /// This constructor is not compatible with the ; - /// use instead. + /// use the constructor instead. /// /// public ParentCommandAttribute(string parentCommandTypeName) diff --git a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs index 4d7a533c..2146abfa 100644 --- a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs @@ -15,6 +15,7 @@ namespace Ookii.CommandLine.Conversion; /// method. /// /// +/// public abstract class ArgumentConverter { /// @@ -26,6 +27,10 @@ public abstract class ArgumentConverter /// 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. /// @@ -37,7 +42,7 @@ public abstract class ArgumentConverter public abstract object? Convert(string value, CultureInfo culture, CommandLineArgument argument); /// - /// Converts a string to the type of the argument. + /// Converts a string span to the type of the argument. /// /// The containing the string to convert. /// The culture to use for the conversion. @@ -53,6 +58,9 @@ public abstract class ArgumentConverter /// type. /// /// + /// + /// or is . + /// /// /// The value was not in a correct format for the target type. /// diff --git a/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs index 3f849c30..43c4388a 100644 --- a/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverterAttribute.cs @@ -10,16 +10,17 @@ namespace Ookii.CommandLine.Conversion; /// /// /// The type specified by this attribute must derive from the -/// class. +/// class, and must convert to the type of the argument the attribute is applied to. /// /// -/// Apply this attribute to the property or method defining the argument to use a custom +/// 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 { @@ -49,6 +50,12 @@ public ArgumentConverterAttribute(Type converterType) /// /// The fully qualified name of the to use as a converter. /// + /// + /// + /// This constructor is not compatible with the ; + /// use the constructor instead. + /// + /// /// /// is /// diff --git a/src/Ookii.CommandLine/Conversion/BooleanConverter.cs b/src/Ookii.CommandLine/Conversion/BooleanConverter.cs index 5647b3cd..38cfbde2 100644 --- a/src/Ookii.CommandLine/Conversion/BooleanConverter.cs +++ b/src/Ookii.CommandLine/Conversion/BooleanConverter.cs @@ -4,8 +4,13 @@ namespace Ookii.CommandLine.Conversion; /// -/// Converter for arguments with boolean values. These are typically switch arguments. +/// 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 { @@ -14,11 +19,46 @@ public class BooleanConverter : ArgumentConverter /// 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/EnumConverter.cs b/src/Ookii.CommandLine/Conversion/EnumConverter.cs index a8c4e5b7..d51f9a53 100644 --- a/src/Ookii.CommandLine/Conversion/EnumConverter.cs +++ b/src/Ookii.CommandLine/Conversion/EnumConverter.cs @@ -8,6 +8,17 @@ namespace Ookii.CommandLine.Conversion; ///
/// /// +/// This converter performs a case insensitive conversion, and accepts the name of an enumeration +/// value, or its underlying value. In the latter case, the value does not need to be one of the +/// defined values of the enumeration; use the +/// attribute to ensure only defined enumeration values can be used. +/// +/// +/// 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. /// @@ -15,24 +26,59 @@ namespace Ookii.CommandLine.Conversion; /// public class EnumConverter : ArgumentConverter { - private readonly Type _enumType; - /// /// 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)); + 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); + return Enum.Parse(EnumType, value, true); } catch (ArgumentException ex) { @@ -45,12 +91,29 @@ public EnumConverter(Type enumType) } #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); + return Enum.Parse(EnumType, value, true); } catch (ArgumentException ex) { @@ -65,7 +128,7 @@ public EnumConverter(Type enumType) private Exception CreateException(string value, Exception inner, CommandLineArgument argument) { - var message = argument.Parser.StringProvider.ValidateEnumValueFailed(argument.ArgumentName, _enumType, value, true); + 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 index 7f5648f6..5bb408ed 100644 --- a/src/Ookii.CommandLine/Conversion/GeneratedConverterNamespaceAttribute.cs +++ b/src/Ookii.CommandLine/Conversion/GeneratedConverterNamespaceAttribute.cs @@ -19,6 +19,7 @@ namespace Ookii.CommandLine.Conversion; /// Use this attribute to modify the namespace used. /// /// +/// [AttributeUsage(AttributeTargets.Assembly)] public sealed class GeneratedConverterNamespaceAttribute : Attribute { @@ -27,9 +28,12 @@ public sealed class GeneratedConverterNamespaceAttribute : Attribute /// with the specified namespace. ///
/// The namespace to use. + /// + /// is . + /// public GeneratedConverterNamespaceAttribute(string @namespace) { - Namespace = @namespace; + Namespace = @namespace ?? throw new ArgumentNullException(nameof(@namespace)); } /// diff --git a/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs index 501c1ca9..50c8553d 100644 --- a/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs +++ b/src/Ookii.CommandLine/Conversion/KeyConverterAttribute.cs @@ -5,12 +5,12 @@ namespace Ookii.CommandLine.Conversion; /// -/// Specifies a to use for the keys of a dictionary argument. +/// Specifies a custom to use for the keys of a dictionary argument. /// /// /// -/// This attribute can be used along with the and -/// attribute to customize the parsing of a dictionary +/// 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 /// . /// @@ -20,9 +20,10 @@ namespace Ookii.CommandLine.Conversion; /// /// /// This attribute is ignored if the argument uses the -/// or if the argument is not a dictionary argument. +/// attribute, or if the argument is not a dictionary argument. /// /// +/// /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] public sealed class KeyConverterAttribute : Attribute @@ -53,6 +54,12 @@ public KeyConverterAttribute(Type converterType) /// /// The fully qualified name of the to use as a converter. /// + /// + /// + /// This constructor is not compatible with the ; + /// use the constructor instead. + /// + /// /// /// is /// diff --git a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs index e4f8241d..2e231782 100644 --- a/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs +++ b/src/Ookii.CommandLine/Conversion/KeyValuePairConverter.cs @@ -26,25 +26,27 @@ public static class KeyValuePairConverter /// /// 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 { - private readonly ArgumentConverter _keyConverter; - private readonly ArgumentConverter _valueConverter; - private readonly bool _allowNullValues; - private readonly string _separator; - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class + /// with the specified key and value converters and options. /// /// - /// Provides the used to convert the key/value pair's keys. + /// The used to convert the key/value pair's keys. /// /// - /// Provides the used to convert the key/value pair's values. + /// The used to convert the key/value pair's values. /// /// - /// Provides an optional custom key/value separator. If , the value + /// An optional custom key/value separator. If , the value /// of is used. /// /// @@ -58,11 +60,11 @@ public class KeyValuePairConverter : ArgumentConverter /// 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) + 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)); } @@ -75,26 +77,110 @@ public KeyValuePairConverter(ArgumentConverter keyConverter, ArgumentConverter v [RequiresUnreferencedCode("Key and value converters cannot be statically determined.")] #endif public KeyValuePairConverter() - : this(typeof(TKey).GetStringConverter(null), typeof(TValue).GetStringConverter(null), null, true) + : 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) { - var (key, valueForKey) = value.SplitOnce(_separator.AsSpan(), out bool hasSeparator); + 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)); + 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) + 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); diff --git a/src/Ookii.CommandLine/Conversion/KeyValueSeparatorAttribute.cs b/src/Ookii.CommandLine/Conversion/KeyValueSeparatorAttribute.cs index f22db716..32bf967d 100644 --- a/src/Ookii.CommandLine/Conversion/KeyValueSeparatorAttribute.cs +++ b/src/Ookii.CommandLine/Conversion/KeyValueSeparatorAttribute.cs @@ -16,6 +16,7 @@ namespace Ookii.CommandLine.Conversion; /// attribute, or if the argument is not a dictionary argument. /// /// +/// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] public class KeyValueSeparatorAttribute : Attribute { @@ -45,5 +46,8 @@ public KeyValueSeparatorAttribute(string 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 index 586df100..5b41ba9a 100644 --- a/src/Ookii.CommandLine/Conversion/NullableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/NullableConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Ookii.CommandLine.Conversion; @@ -10,7 +11,7 @@ namespace Ookii.CommandLine.Conversion; /// /// 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 standard . +/// of the . /// /// /// @@ -28,8 +29,17 @@ public NullableConverter(ArgumentConverter baseConverter) } /// + /// + /// 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; @@ -39,6 +49,10 @@ public NullableConverter(ArgumentConverter baseConverter) } /// + /// + /// 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) diff --git a/src/Ookii.CommandLine/Conversion/ParsableConverter.cs b/src/Ookii.CommandLine/Conversion/ParsableConverter.cs index b009ac13..bf8c39d4 100644 --- a/src/Ookii.CommandLine/Conversion/ParsableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ParsableConverter.cs @@ -6,17 +6,18 @@ namespace Ookii.CommandLine.Conversion; /// -/// An argument converter for types that implement . +/// An argument converter for types that implement the interface. /// -/// The type to convert. +/// The type to convert to. /// /// /// Conversion is performed using the method. /// /// -/// Only use this converter for types that implement , but not -/// . For types that implement , -/// use the . +/// Only use this converter for types that implement the interface, +/// but not the interface. For types that implement the +/// interface, use the +/// class. /// /// /// diff --git a/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs index b00d1620..10eefd53 100644 --- a/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/SpanParsableConverter.cs @@ -6,16 +6,17 @@ namespace Ookii.CommandLine.Conversion; /// -/// An argument converter for types that implement . +/// An argument converter for types that implement the interface. /// -/// The type to convert. +/// The type to convert to. /// /// -/// Conversion is performed using the method. +/// Conversion is performed using the +/// method. /// /// -/// For types that implement , but not , -/// use the . +/// For types that implement the interface, but not the +/// interface, use the class. /// /// /// diff --git a/src/Ookii.CommandLine/Conversion/StringConverter.cs b/src/Ookii.CommandLine/Conversion/StringConverter.cs index b549638c..5db9160d 100644 --- a/src/Ookii.CommandLine/Conversion/StringConverter.cs +++ b/src/Ookii.CommandLine/Conversion/StringConverter.cs @@ -7,7 +7,7 @@ namespace Ookii.CommandLine.Conversion; /// A converter for arguments with string values. /// /// -/// This converter does not performan any actual conversion, and returns the existing string as-is. +/// 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. /// @@ -19,6 +19,17 @@ public class StringConverter : ArgumentConverter ///
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/TypeConverterArgumentConverter.cs b/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs index fbdb9bb4..8c0e3917 100644 --- a/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs +++ b/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs @@ -5,7 +5,7 @@ namespace Ookii.CommandLine.Conversion; /// -/// A that wraps an existing for a +/// An that wraps an existing for a /// type. /// /// @@ -14,6 +14,7 @@ namespace Ookii.CommandLine.Conversion; /// class. ///
/// +/// public class TypeConverterArgumentConverter : ArgumentConverter { /// diff --git a/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverterGeneric.cs b/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverterGeneric.cs index 1269e43e..79edb94a 100644 --- a/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverterGeneric.cs +++ b/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverterGeneric.cs @@ -8,6 +8,7 @@ namespace Ookii.CommandLine.Conversion; /// type. /// /// The type to convert to. +/// #if NET6_0_OR_GREATER [RequiresUnreferencedCode("Determining the TypeConverter for a type may require the type to be annotated.")] #endif diff --git a/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs b/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs index b5dfc21f..43568b31 100644 --- a/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs +++ b/src/Ookii.CommandLine/Conversion/ValueConverterAttribute.cs @@ -5,12 +5,12 @@ namespace Ookii.CommandLine.Conversion; /// -/// Specifies a to use for the keys of a dictionary argument. +/// Specifies a custom to use for the keys of a dictionary argument. /// /// /// /// This attribute can be used along with the and -/// attribute to customize the parsing of a dictionary +/// attributes to customize the parsing of a dictionary /// argument without having to write a custom that returns a /// . /// @@ -23,6 +23,7 @@ namespace Ookii.CommandLine.Conversion; /// or if the argument is not a dictionary argument. /// /// +/// /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] public sealed class ValueConverterAttribute : Attribute @@ -53,6 +54,12 @@ public ValueConverterAttribute(Type converterType) /// /// The fully qualified name of the to use as a converter. /// + /// + /// + /// This constructor is not compatible with the ; + /// use the constructor instead. + /// + /// /// /// is /// diff --git a/src/Ookii.CommandLine/Properties/Resources.Designer.cs b/src/Ookii.CommandLine/Properties/Resources.Designer.cs index 04834395..5f491e31 100644 --- a/src/Ookii.CommandLine/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine/Properties/Resources.Designer.cs @@ -582,6 +582,15 @@ internal static string TypeIsNotCommandFormat { } } + /// + /// Looks up a localized string similar to The type '{0}' is not an enumeration type.. + /// + internal static string TypeIsNotEnumFormat { + get { + return ResourceManager.GetString("TypeIsNotEnumFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to The type must be a generic type definition.. /// diff --git a/src/Ookii.CommandLine/Properties/Resources.resx b/src/Ookii.CommandLine/Properties/Resources.resx index 05a22d21..ac855b2d 100644 --- a/src/Ookii.CommandLine/Properties/Resources.resx +++ b/src/Ookii.CommandLine/Properties/Resources.resx @@ -405,4 +405,7 @@ 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 From 36cdd45cab6d18edcfed2ac131f67a44c5965232 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 27 Jun 2023 18:23:19 -0700 Subject: [PATCH 197/234] Handle OverflowException for type converters. --- .../ConverterGenerator.cs | 4 ++++ .../CommandLineParserTest.cs | 13 ++++++++++++ src/Ookii.CommandLine/CommandLineArgument.cs | 12 +++++++++++ .../Conversion/ArgumentConverter.cs | 18 ++++++++++------ .../Conversion/ConstructorConverter.cs | 4 ++++ .../Conversion/NullableConverter.cs | 21 +++++++++++++------ .../Conversion/ParseConverter.cs | 4 ++++ 7 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 84ad32fd..e07b91d0 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -204,6 +204,10 @@ private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, Con builder.OpenBlock(); builder.AppendLine("throw;"); builder.CloseBlock(); // catch + builder.AppendLine("catch (System.OverflowException)"); + builder.OpenBlock(); + builder.AppendLine("throw;"); + builder.CloseBlock(); // catch builder.AppendLine("catch (System.Exception ex)"); builder.OpenBlock(); builder.AppendLine("throw new System.FormatException(ex.Message, ex);"); diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index 3fd317c3..cc12b79c 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1,4 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using Ookii.CommandLine.Conversion; using Ookii.CommandLine.Support; using Ookii.CommandLine.Tests.Commands; using System; @@ -1201,6 +1202,18 @@ public void TestConversion(ProviderKind kind) 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] + [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] diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 61026393..85e5b3e5 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -605,6 +605,14 @@ public Type ArgumentType /// public Type ElementType => _elementType; + /// + /// Gets the converter used to convert string values to the argument's type. + /// + /// + /// The for this argument. + /// + public ArgumentConverter Converter => _converter; + /// /// Gets the position of this argument. /// @@ -1259,6 +1267,10 @@ private static string GetFriendlyTypeName(Type type) { throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); } + catch (OverflowException ex) + { + throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); + } } internal bool HasInformation(UsageWriter writer) diff --git a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs index 2146abfa..a402349d 100644 --- a/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ArgumentConverter.cs @@ -34,10 +34,13 @@ public abstract class ArgumentConverter /// /// 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 , - /// a thrown by this method will be passed down to - /// the user unmodified. + /// 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); @@ -64,10 +67,13 @@ public abstract class ArgumentConverter /// /// 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 , - /// a thrown by this method will be passed down to - /// the user unmodified. + /// 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) { diff --git a/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs index cf918c9a..1b92e38a 100644 --- a/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs @@ -36,6 +36,10 @@ public ConstructorConverter( { throw; } + catch (OverflowException) + { + throw; + } catch (Exception ex) { // Since we don't know what the constructor will throw, we'll wrap anything in a diff --git a/src/Ookii.CommandLine/Conversion/NullableConverter.cs b/src/Ookii.CommandLine/Conversion/NullableConverter.cs index 5b41ba9a..b204910b 100644 --- a/src/Ookii.CommandLine/Conversion/NullableConverter.cs +++ b/src/Ookii.CommandLine/Conversion/NullableConverter.cs @@ -17,17 +17,26 @@ namespace Ookii.CommandLine.Conversion; /// public class NullableConverter : ArgumentConverter { - private readonly ArgumentConverter _baseConverter; - /// /// Initializes a new instance of the class. /// - /// The converter to use for the type T. + /// The converter to use for the target type. + /// + /// is . + /// public NullableConverter(ArgumentConverter baseConverter) { - _baseConverter = 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 @@ -45,7 +54,7 @@ public NullableConverter(ArgumentConverter baseConverter) return null; } - return _baseConverter.Convert(value, culture, argument); + return BaseConverter.Convert(value, culture, argument); } /// @@ -60,6 +69,6 @@ public NullableConverter(ArgumentConverter baseConverter) return null; } - return _baseConverter.Convert(value, culture, argument); + return BaseConverter.Convert(value, culture, argument); } } diff --git a/src/Ookii.CommandLine/Conversion/ParseConverter.cs b/src/Ookii.CommandLine/Conversion/ParseConverter.cs index 5e9dee63..1246b00d 100644 --- a/src/Ookii.CommandLine/Conversion/ParseConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ParseConverter.cs @@ -34,6 +34,10 @@ public ParseConverter(MethodInfo method, bool hasCulture) { throw; } + catch (OverflowException) + { + throw; + } catch (Exception ex) { // Since we don't know what the method will throw, we'll wrap anything in a From 2e695bc36d8765d5336713e99ccc56ac40d21022 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 28 Jun 2023 12:40:35 -0700 Subject: [PATCH 198/234] Simplify exception handling for ArgumentConverter. --- .../ConverterGenerator.cs | 26 ++-------------- src/Ookii.CommandLine/CommandLineArgument.cs | 17 +++++----- .../Conversion/ConstructorConverter.cs | 31 +++++++++---------- .../Conversion/ParseConverter.cs | 27 +++++++--------- src/Ookii.CommandLine/TypeHelper.cs | 14 --------- 5 files changed, 37 insertions(+), 78 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index e07b91d0..33dbd673 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -182,37 +182,15 @@ private static void CreateConverter(SourceBuilder builder, ITypeSymbol type, Con string inputType = info.UseSpan ? "System.ReadOnlySpan" : "string"; string culture = info.HasCulture ? ", culture" : string.Empty; builder.AppendLine($"public override object? Convert({inputType} value, System.Globalization.CultureInfo culture, Ookii.CommandLine.CommandLineArgument argument)"); - builder.OpenBlock(); - builder.AppendLine("try"); - builder.OpenBlock(); if (info.ParseMethod) { - builder.AppendLine($"return {type.ToQualifiedName()}.Parse(value{culture});"); + builder.AppendLine($" => {type.ToQualifiedName()}.Parse(value{culture});"); } else { - builder.AppendLine($"return new {type.ToQualifiedName()}(value);"); + builder.AppendLine($" => new {type.ToQualifiedName()}(value);"); } - builder.CloseBlock(); // try - builder.AppendLine("catch (Ookii.CommandLine.CommandLineArgumentException ex)"); - builder.OpenBlock(); - // Patch the exception with the argument name. - builder.AppendLine("throw new Ookii.CommandLine.CommandLineArgumentException(ex.Message, argument.ArgumentName, ex.Category, ex.InnerException);"); - builder.CloseBlock(); // catch - builder.AppendLine("catch (System.FormatException)"); - builder.OpenBlock(); - builder.AppendLine("throw;"); - builder.CloseBlock(); // catch - builder.AppendLine("catch (System.OverflowException)"); - builder.OpenBlock(); - builder.AppendLine("throw;"); - builder.CloseBlock(); // catch - builder.AppendLine("catch (System.Exception ex)"); - builder.OpenBlock(); - builder.AppendLine("throw new System.FormatException(ex.Message, ex);"); - builder.CloseBlock(); // catch - builder.CloseBlock(); // Convert method if (info.UseSpan) { builder.AppendLine(); diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index 85e5b3e5..c081fc83 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -1259,16 +1259,19 @@ private static string GetFriendlyTypeName(Type type) return converted; } - catch (NotSupportedException ex) + catch (CommandLineArgumentException ex) { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); - } - catch (FormatException ex) - { - throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); + if (ex.ArgumentName == ArgumentName) + { + throw; + } + + // Patch with the correct argument name. + throw new CommandLineArgumentException(ex.Message, ArgumentName, ex.Category, ex); } - catch (OverflowException ex) + catch (Exception ex) { + // Wrap any other exception in a CommandLineArgumentException. throw _parser.StringProvider.CreateException(CommandLineArgumentErrorCategory.ArgumentValueConversion, ex, this, stringValue ?? spanValue.ToString()); } } diff --git a/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs index 1b92e38a..e2fe68fb 100644 --- a/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ConstructorConverter.cs @@ -1,6 +1,8 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Reflection; +using System.Runtime.ExceptionServices; namespace Ookii.CommandLine.Conversion; @@ -25,26 +27,21 @@ public ConstructorConverter( { try { - return _type.CreateInstance(value); + // Since we are passing BindingFlags.Public, the correct annotation is present. + return Activator.CreateInstance(_type, value); } - catch (CommandLineArgumentException ex) - { - // Patch the exception with the argument name. - throw new CommandLineArgumentException(ex.Message, argument.ArgumentName, ex.Category, ex.InnerException); - } - catch (FormatException) - { - throw; - } - catch (OverflowException) + catch (TargetInvocationException ex) { + if (ex.InnerException == null) + { + throw; + } + + // Rethrow inner exception with original call stack. + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + + // Actually unreachable. throw; } - 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/ParseConverter.cs b/src/Ookii.CommandLine/Conversion/ParseConverter.cs index 1246b00d..ecb8daac 100644 --- a/src/Ookii.CommandLine/Conversion/ParseConverter.cs +++ b/src/Ookii.CommandLine/Conversion/ParseConverter.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Reflection; +using System.Runtime.ExceptionServices; namespace Ookii.CommandLine.Conversion; @@ -25,24 +26,18 @@ public ParseConverter(MethodInfo method, bool hasCulture) { return _method.Invoke(null, parameters); } - catch (CommandLineArgumentException ex) - { - // Patch the exception with the argument name. - throw new CommandLineArgumentException(ex.Message, argument.ArgumentName, ex.Category, ex.InnerException); - } - catch (FormatException) - { - throw; - } - catch (OverflowException) + catch (TargetInvocationException ex) { + if (ex.InnerException == null) + { + throw; + } + + // Rethrow inner exception with original call stack. + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + + // Actually unreachable. throw; } - 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/TypeHelper.cs b/src/Ookii.CommandLine/TypeHelper.cs index 05fb666a..5166375d 100644 --- a/src/Ookii.CommandLine/TypeHelper.cs +++ b/src/Ookii.CommandLine/TypeHelper.cs @@ -73,20 +73,6 @@ public static bool ImplementsInterface( return Activator.CreateInstance(type); } - public static object? CreateInstance( -#if NET6_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - this Type type, params object?[]? args) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - return Activator.CreateInstance(type, args); - } - #if NET6_0_OR_GREATER [RequiresUnreferencedCode("Argument information cannot be statically determined using reflection. Consider using the GeneratedParserAttribute.", Url = CommandLineParser.UnreferencedCodeHelpUrl)] #endif From cd11eec325d2d3767bc1d4d25a530398177e5e22 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 28 Jun 2023 15:49:35 -0700 Subject: [PATCH 199/234] Updated XML comments for the Support namespace. --- src/Ookii.CommandLine/Support/ArgumentProvider.cs | 9 +++++---- src/Ookii.CommandLine/Support/CommandProvider.cs | 3 ++- src/Ookii.CommandLine/Support/GeneratedArgument.cs | 7 ++++++- .../Support/GeneratedArgumentProvider.cs | 6 +++++- src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs | 7 ++++++- .../Support/GeneratedCommandInfoWithCustomParsing.cs | 8 +++++++- src/Ookii.CommandLine/Support/ProviderKind.cs | 8 +++++--- 7 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/Ookii.CommandLine/Support/ArgumentProvider.cs b/src/Ookii.CommandLine/Support/ArgumentProvider.cs index a58acdb1..c40bd54d 100644 --- a/src/Ookii.CommandLine/Support/ArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/ArgumentProvider.cs @@ -9,9 +9,10 @@ namespace Ookii.CommandLine.Support; /// A source of arguments for the . ///
/// -/// This class is used by the source generator when using +/// 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; @@ -74,10 +75,10 @@ protected ArgumentProvider(Type argumentsType, ParseOptionsAttribute? options, I public ParseOptionsAttribute? OptionsAttribute { get; } /// - /// Gets a value that indicates whether this arguments type is a shell command. + /// Gets a value that indicates whether this arguments type is a subcommand. /// /// - /// if the arguments type is a shell command; otherwise, . + /// if the arguments type is a subcommand; otherwise, . /// public abstract bool IsCommand { get; } @@ -117,6 +118,6 @@ public void RunValidators(CommandLineParser parser) /// if there are no required properties, or if the property equals /// . /// - /// An instance of the type indicated by . + /// 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 index e4616298..4a71e4da 100644 --- a/src/Ookii.CommandLine/Support/CommandProvider.cs +++ b/src/Ookii.CommandLine/Support/CommandProvider.cs @@ -7,9 +7,10 @@ namespace Ookii.CommandLine.Support; /// A source of commands for the . /// /// -/// This class is used by the source generator when using +/// 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 { /// diff --git a/src/Ookii.CommandLine/Support/GeneratedArgument.cs b/src/Ookii.CommandLine/Support/GeneratedArgument.cs index 4dcb390d..d866abce 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgument.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgument.cs @@ -8,8 +8,13 @@ namespace Ookii.CommandLine.Support; /// -/// This class is for internal use by the source generator, and should not be used in your code. +/// 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; diff --git a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs index 803a8e07..e2d4b446 100644 --- a/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs +++ b/src/Ookii.CommandLine/Support/GeneratedArgumentProvider.cs @@ -8,8 +8,12 @@ namespace Ookii.CommandLine.Support; /// /// A base class for argument providers created by the . -/// This type is for internal use only and should not be used by your code. /// +/// +/// 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; diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs index 42c7c5dd..3d2f812c 100644 --- a/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfo.cs @@ -7,8 +7,13 @@ namespace Ookii.CommandLine.Support; /// -/// This class is for internal use by the source generator, and should not be used in your code. +/// 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; diff --git a/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs index ad6f0160..4b9f5f4e 100644 --- a/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs +++ b/src/Ookii.CommandLine/Support/GeneratedCommandInfoWithCustomParsing.cs @@ -6,9 +6,15 @@ namespace Ookii.CommandLine.Support; /// -/// This class is for internal use by the source generator, and should not be used in your code. +/// 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() { diff --git a/src/Ookii.CommandLine/Support/ProviderKind.cs b/src/Ookii.CommandLine/Support/ProviderKind.cs index 3435865b..eb2d98a2 100644 --- a/src/Ookii.CommandLine/Support/ProviderKind.cs +++ b/src/Ookii.CommandLine/Support/ProviderKind.cs @@ -1,7 +1,7 @@ namespace Ookii.CommandLine.Support; /// -/// Specifies the kind of provider that was the source of the arguments. +/// Specifies the kind of provider that was the source of the arguments or subcommands. /// public enum ProviderKind { @@ -10,11 +10,13 @@ public enum ProviderKind /// Unknown, /// - /// An argument provider that uses reflection. + /// An provider that uses reflection. /// Reflection, /// - /// An argument provider that uses code generation. + /// An provider that uses source generation. These are typically created using the + /// and + /// attributes. /// Generated } From 86a1154c32ca555c4cc1b20b559dca4eb65f2f5c Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 28 Jun 2023 16:01:45 -0700 Subject: [PATCH 200/234] Updated XML comments for Terminal namespace. --- docs/Ookii.CommandLine.shfbproj | 2 +- src/Ookii.CommandLine/Terminal/TextFormat.cs | 25 ++++++++++--------- .../Terminal/VirtualTerminal.cs | 1 + .../Terminal/VirtualTerminalSupport.cs | 21 +++++++++++++++- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/Ookii.CommandLine.shfbproj b/docs/Ookii.CommandLine.shfbproj index 297a8456..87ad0203 100644 --- a/docs/Ookii.CommandLine.shfbproj +++ b/docs/Ookii.CommandLine.shfbproj @@ -31,7 +31,7 @@ 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. diff --git a/src/Ookii.CommandLine/Terminal/TextFormat.cs b/src/Ookii.CommandLine/Terminal/TextFormat.cs index 5d5c4b64..23ff7f62 100644 --- a/src/Ookii.CommandLine/Terminal/TextFormat.cs +++ b/src/Ookii.CommandLine/Terminal/TextFormat.cs @@ -8,9 +8,9 @@ namespace Ookii.CommandLine.Terminal; /// /// /// -/// Write one of the predefined values in this class to a stream representing the console, such -/// as or , to set the specified text format -/// on that stream. +/// 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 @@ -22,6 +22,7 @@ namespace Ookii.CommandLine.Terminal; /// the method or the operator. /// /// +/// public readonly struct TextFormat : IEquatable { /// @@ -33,23 +34,23 @@ namespace Ookii.CommandLine.Terminal; /// public static readonly TextFormat BoldBright = new("\x1b[1m"); /// - /// Removes the brightness/intensity flag to the foreground color. + /// Removes the brightness/intensity flag from the foreground color. /// public static readonly TextFormat NoBoldBright = new("\x1b[22m"); /// - /// Adds underline. + /// Adds underlining to the text. /// public static readonly TextFormat Underline = new("\x1b[4m"); /// - /// Removes underline. + /// Removes underlining from the text. /// public static readonly TextFormat NoUnderline = new("\x1b[24m"); /// - /// Swaps foreground and background colors. + /// Swaps the foreground and background colors. /// public static readonly TextFormat Negative = new("\x1b[7m"); /// - /// Returns foreground and background colors to normal. + /// Returns the foreground and background colors to their normal, non-swapped state. /// public static readonly TextFormat Positive = new("\x1b[27m"); /// @@ -192,8 +193,8 @@ namespace Ookii.CommandLine.Terminal; private readonly string? _value; /// - /// Returns the virtual terminal sequence to the foreground or background color to an RGB - /// color. + /// Returns a virtual terminal sequence that can be used to set the foreground or background + /// color to an RGB color. /// /// The color to use. /// @@ -267,7 +268,7 @@ public override bool Equals(object? obj) public static TextFormat operator +(TextFormat left, TextFormat right) => left.Combine(right); /// - /// Determine whether this instance and another instance have the + /// Determines whether this instance and another instance have the /// same value. /// /// The first value. @@ -278,7 +279,7 @@ public override bool Equals(object? obj) public static bool operator ==(TextFormat left, TextFormat right) => left.Equals(right); /// - /// Determine whether this instance and another instance have a + /// Determines whether this instance and another instance have a /// different value. /// /// The first value. diff --git a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs index b931a1e0..d4e52022 100644 --- a/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs +++ b/src/Ookii.CommandLine/Terminal/VirtualTerminal.cs @@ -12,6 +12,7 @@ namespace Ookii.CommandLine.Terminal; /// if enabled by the class. /// /// +/// public static class VirtualTerminal { /// diff --git a/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs b/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs index 859ac768..c4387ce1 100644 --- a/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs +++ b/src/Ookii.CommandLine/Terminal/VirtualTerminalSupport.cs @@ -9,6 +9,7 @@ namespace Ookii.CommandLine.Terminal; /// 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; @@ -31,6 +32,14 @@ internal VirtualTerminalSupport(IntPtr handle, NativeMethods.ConsoleModes previo /// /// 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. + /// + /// ~VirtualTerminalSupport() { ResetConsoleMode(); @@ -45,7 +54,17 @@ internal VirtualTerminalSupport(IntPtr handle, NativeMethods.ConsoleModes previo /// public bool IsSupported => _supported; - /// + /// + /// 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(); From 5acac8eb4d142c58dd3cf8b7914a41dfd72d4dee Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 28 Jun 2023 18:36:48 -0700 Subject: [PATCH 201/234] Xml comment updates for the Validation namespace --- .../Validation/ArgumentValidationAttribute.cs | 19 ++++++++++--------- .../ArgumentValidationWithHelpAttribute.cs | 5 +++-- .../Validation/ClassValidationAttribute.cs | 17 +++++++++-------- .../DependencyValidationAttribute.cs | 7 ++++--- .../Validation/ProhibitsAttribute.cs | 19 ++++++++++--------- .../Validation/RequiresAnyAttribute.cs | 12 ++++++------ .../Validation/RequiresAttribute.cs | 5 +++-- .../Validation/ValidateCountAttribute.cs | 8 ++++---- .../Validation/ValidateEnumValueAttribute.cs | 9 +++++---- 9 files changed, 54 insertions(+), 47 deletions(-) diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs index dfe8bf8b..53a73d5e 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs @@ -12,12 +12,13 @@ namespace Ookii.CommandLine.Validation; /// 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 +/// If validation fails, the validator 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. +/// 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 @@ -52,7 +53,7 @@ public abstract class ArgumentValidationAttribute : Attribute /// /// The argument being validated. /// - /// The argument value. If not , this must be an instance of + /// The argument value. If not , this must be a string or an instance of /// . /// /// @@ -82,7 +83,7 @@ public void Validate(CommandLineArgument argument, object? value) /// /// /// if validation was performed and successful; - /// if this validator doesn't support validating spans and the + /// if this validator doesn't support validating spans and the /// method should be used instead. /// /// @@ -133,7 +134,7 @@ public bool ValidateSpan(CommandLineArgument argument, ReadOnlySpan value) /// If the property is , /// 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 + /// arguments, the parameter will be equal to the last value added /// to the collection or dictionary. /// /// @@ -198,7 +199,7 @@ public virtual string GetErrorMessage(CommandLineArgument argument, object? valu /// /// Gets the usage help message for this validator. /// - /// The argument is the validator is for. + /// The argument that the validator is for. /// /// The usage help message, or if there is none. The /// base implementation always returns . diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs index 2754f10c..84509c9e 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationWithHelpAttribute.cs @@ -11,6 +11,7 @@ /// This class just adds some common functionality to make it easier. /// /// +/// public abstract class ArgumentValidationWithHelpAttribute : ArgumentValidationAttribute { /// @@ -35,7 +36,7 @@ public abstract class ArgumentValidationWithHelpAttribute : ArgumentValidationAt /// /// Gets the usage help message for this validator. /// - /// The argument is the validator is for. + /// The argument that the validator is for. /// /// The usage help message, or if the /// property is . @@ -53,7 +54,7 @@ public abstract class ArgumentValidationWithHelpAttribute : ArgumentValidationAt /// /// Gets the usage help message for this validator. /// - /// The argument is the validator is for. + /// The argument that the validator is for. /// /// The usage help message. /// diff --git a/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs index d57ab73a..c46f7af8 100644 --- a/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs @@ -14,12 +14,13 @@ namespace Ookii.CommandLine.Validation; /// 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 +/// If validation fails, the validator 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. +/// 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 @@ -65,7 +66,7 @@ public void Validate(CommandLineParser parser) /// /// Gets the error message to display if validation failed. /// - /// The argument parser that was validated. + /// The command line parser that was validated. /// The error message. /// /// @@ -79,7 +80,7 @@ public virtual string GetErrorMessage(CommandLineParser parser) /// /// When overridden in a derived class, determines if the arguments are valid. /// - /// The argument parser being validated. + /// The command line parser being validated. /// /// if the arguments are valid; otherwise, . /// @@ -88,7 +89,7 @@ public virtual string GetErrorMessage(CommandLineParser parser) /// /// Gets the usage help message for this validator. /// - /// The parser is the validator is for. + /// The command line parser that the validator is for. /// /// The usage help message, or if there is none. The /// base implementation always returns . diff --git a/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs b/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs index fa6d7fac..bda73858 100644 --- a/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/DependencyValidationAttribute.cs @@ -9,6 +9,7 @@ namespace Ookii.CommandLine.Validation; /// /// Base class for the and class. /// +/// public abstract class DependencyValidationAttribute : ArgumentValidationWithHelpAttribute { private readonly string? _argument; @@ -26,7 +27,7 @@ public abstract class DependencyValidationAttribute : ArgumentValidationWithHelp /// /// is . /// - public DependencyValidationAttribute(bool requires, string argument) + protected DependencyValidationAttribute(bool requires, string argument) { _argument = argument ?? throw new ArgumentNullException(nameof(argument)); _requires = requires; @@ -44,7 +45,7 @@ public DependencyValidationAttribute(bool requires, string argument) /// /// is . /// - public DependencyValidationAttribute(bool requires, params string[] arguments) + protected DependencyValidationAttribute(bool requires, params string[] arguments) { _arguments = arguments ?? throw new ArgumentNullException(nameof(arguments)); _requires = requires; @@ -102,7 +103,7 @@ public sealed override bool IsValid(CommandLineArgument argument, object? value) /// /// Resolves the argument names in the property to their actual - /// property. + /// instances. /// /// The instance. /// A list of the arguments. diff --git a/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs b/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs index 55af06b5..9e1dcb69 100644 --- a/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs @@ -3,13 +3,13 @@ namespace Ookii.CommandLine.Validation; /// -/// Validates that an argument cannot be used together with other arguments. +/// 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 in combination -/// with one or more other attributes. If one or more of the dependencies does not have -/// a value, validation will fail. +/// 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. @@ -19,15 +19,16 @@ namespace Ookii.CommandLine.Validation; /// error category set to . /// /// -/// 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. +/// 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 { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The name of the argument that this argument prohibits. /// @@ -39,8 +40,8 @@ public ProhibitsAttribute(string argument) } /// - /// Initializes a new instance of the class with multiple - /// dependencies. + /// Initializes a new instance of the class with multiple + /// prohibited arguments. /// /// The names of the arguments that this argument prohibits. /// diff --git a/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs b/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs index fcd8e8a6..88ea08a7 100644 --- a/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs @@ -42,11 +42,11 @@ namespace Ookii.CommandLine.Validation; /// /// /// 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 . +/// 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, it is assumed to have no value. +/// specified arguments does not exist, an exception is thrown during validation. /// /// /// @@ -145,10 +145,10 @@ public override CommandLineArgumentErrorCategory ErrorCategory public bool IncludeInUsageHelp { get; set; } = true; /// - /// Determines if the at least one of the arguments in was + /// Determines if at least one of the arguments in the property was /// supplied on the command line. /// - /// The argument parser being validated. + /// The command line parser being validated. /// /// if the arguments are valid; otherwise, . /// @@ -162,7 +162,7 @@ public override string GetErrorMessage(CommandLineParser parser) /// /// Gets the usage help message for this validator. /// - /// The parser is the validator is for. + /// The command line parser that the validator is for. /// /// The usage help message, or if the /// property is . @@ -172,7 +172,7 @@ public override string GetErrorMessage(CommandLineParser parser) /// /// Resolves the argument names in the property to their actual - /// property. + /// instances. /// /// The instance. /// A list of the arguments. diff --git a/src/Ookii.CommandLine/Validation/RequiresAttribute.cs b/src/Ookii.CommandLine/Validation/RequiresAttribute.cs index 63574321..85aea3d5 100644 --- a/src/Ookii.CommandLine/Validation/RequiresAttribute.cs +++ b/src/Ookii.CommandLine/Validation/RequiresAttribute.cs @@ -8,7 +8,7 @@ namespace Ookii.CommandLine.Validation; /// /// /// 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 +/// with one or more other arguments. If one or more of the dependencies does not have /// a value, validation will fail. /// /// @@ -20,7 +20,8 @@ namespace Ookii.CommandLine.Validation; /// /// /// 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. +/// created. If one of the specified arguments does not exist, an exception is thrown during +/// validation. /// /// /// diff --git a/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs index f0e39584..9508c152 100644 --- a/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs @@ -8,9 +8,9 @@ namespace Ookii.CommandLine.Validation; /// /// /// -/// 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. +/// 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. @@ -27,7 +27,7 @@ public class ValidateCountAttribute : ArgumentValidationWithHelpAttribute private readonly int _maximum; /// - /// Initializes a new instance of the class. + /// 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. diff --git a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs index bd600deb..ba64669f 100644 --- a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs @@ -10,10 +10,10 @@ namespace Ookii.CommandLine.Validation; /// /// /// -/// 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. +/// 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 @@ -36,6 +36,7 @@ namespace Ookii.CommandLine.Validation; /// It is an error to use this validator on an argument whose type is not an enumeration. /// /// +/// public class ValidateEnumValueAttribute : ArgumentValidationWithHelpAttribute { /// From 0eea1e3c1a686a7ee83e5f3539076662750267f2 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 5 Jul 2023 16:54:57 -0700 Subject: [PATCH 202/234] More XML comment updates for the Validation namespace. --- .../ArgumentValidatorTest.cs | 4 +++ .../Validation/ProhibitsAttribute.cs | 14 +++++++++ .../Validation/RequiresAnyAttribute.cs | 14 +++++++++ .../Validation/RequiresAttribute.cs | 14 +++++++++ .../Validation/ValidateCountAttribute.cs | 22 ++++++++++--- .../Validation/ValidateEnumValueAttribute.cs | 14 +++++++++ .../Validation/ValidateNotEmptyAttribute.cs | 4 +-- .../Validation/ValidateNotNullAttribute.cs | 15 ++++++--- .../ValidateNotWhiteSpaceAttribute.cs | 25 ++++++++++++--- .../Validation/ValidatePatternAttribute.cs | 31 ++++++++++++++----- .../Validation/ValidateRangeAttribute.cs | 14 +++++++++ .../ValidateStringLengthAttribute.cs | 18 +++++++++-- .../Validation/ValidationMode.cs | 2 +- 13 files changed, 165 insertions(+), 26 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs index f711c3b8..603c0ad6 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentValidatorTest.cs @@ -118,6 +118,10 @@ public void ValidatePatternAttribute() 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] diff --git a/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs b/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs index 9e1dcb69..80bae5bd 100644 --- a/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ProhibitsAttribute.cs @@ -58,10 +58,24 @@ public ProhibitsAttribute(params string[] arguments) /// 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.ValidateProhibitsFailed(argument.MemberName, GetArguments(argument.Parser)); /// + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// 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 88ea08a7..b2a92d3f 100644 --- a/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/RequiresAnyAttribute.cs @@ -156,6 +156,13 @@ 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)); @@ -167,6 +174,13 @@ public override string GetErrorMessage(CommandLineParser parser) /// 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; diff --git a/src/Ookii.CommandLine/Validation/RequiresAttribute.cs b/src/Ookii.CommandLine/Validation/RequiresAttribute.cs index 85aea3d5..284b0db1 100644 --- a/src/Ookii.CommandLine/Validation/RequiresAttribute.cs +++ b/src/Ookii.CommandLine/Validation/RequiresAttribute.cs @@ -58,10 +58,24 @@ public RequiresAttribute(params string[] arguments) /// 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.ValidateRequiresFailed(argument.MemberName, GetArguments(argument.Parser)); /// + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// 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 9508c152..70526e7e 100644 --- a/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateCountAttribute.cs @@ -46,18 +46,18 @@ public ValidateCountAttribute(int minimum, int maximum = int.MaxValue) public override ValidationMode Mode => ValidationMode.AfterParsing; /// - /// Gets the inclusive lower bound on the string length. + /// Gets the inclusive lower bound on the number of elements. /// /// - /// The inclusive lower bound on the string length. + /// The inclusive lower bound on the number of elements. /// public int Minimum => _minimum; /// - /// Get the inclusive upper bound on the string length. + /// Get the inclusive upper bound on the number of elements. /// /// - /// The inclusive upper bound on the string length. + /// The inclusive upper bound on the number of elements. /// public int Maximum => _maximum; @@ -89,10 +89,24 @@ public override bool IsValid(CommandLineArgument argument, object? value) /// 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 ba64669f..409a34ca 100644 --- a/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateEnumValueAttribute.cs @@ -73,10 +73,24 @@ public override bool IsValid(CommandLineArgument argument, object? value) public bool IncludeValuesInErrorMessage { get; set; } /// + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// protected override string GetUsageHelpCore(CommandLineArgument argument) => argument.Parser.StringProvider.ValidateEnumValueUsageHelp(argument.ElementType); /// + /// + /// + /// Use a custom class that overrides the + /// method + /// to customize this message. + /// + /// 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 b7154001..1d1f4cf2 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotEmptyAttribute.cs @@ -12,7 +12,7 @@ namespace Ookii.CommandLine.Validation; /// /// /// 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 +/// so the value may still be an empty string if the argument is not supplied, if that /// is the default value. /// ///
@@ -28,7 +28,7 @@ public class ValidateNotEmptyAttribute : ArgumentValidationWithHelpAttribute public override ValidationMode Mode => ValidationMode.BeforeConversion; /// - /// Determines if the argument is valid. + /// Determines if the argument is not an empty string. /// /// The argument being validated. /// diff --git a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs index a529a356..7d40a6fa 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotNullAttribute.cs @@ -16,7 +16,7 @@ namespace Ookii.CommandLine.Validation; ///
/// /// It is not necessary to use this attribute on required arguments with types that can't be -/// , such as value types (except , and if +/// , 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. /// @@ -33,7 +33,7 @@ namespace Ookii.CommandLine.Validation; public class ValidateNotNullAttribute : ArgumentValidationAttribute { /// - /// Determines if the argument's value is not null. + /// Determines if the argument's value is not . /// /// The argument being validated. /// @@ -54,8 +54,13 @@ public override bool IsValid(CommandLineArgument argument, object? value) /// 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) - { - return argument.Parser.StringProvider.NullArgumentValue(argument.ArgumentName); - } + => argument.Parser.StringProvider.NullArgumentValue(argument.ArgumentName); } diff --git a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs index 457a3a54..84cc0575 100644 --- a/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateNotWhiteSpaceAttribute.cs @@ -9,13 +9,13 @@ namespace Ookii.CommandLine.Validation; /// /// /// -/// 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 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. +/// 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. /// /// /// @@ -30,7 +30,8 @@ public class ValidateNotWhiteSpaceAttribute : ArgumentValidationWithHelpAttribut public override ValidationMode Mode => ValidationMode.BeforeConversion; /// - /// Determines if the argument's value is not null or 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. /// @@ -54,6 +55,13 @@ public override bool IsValid(CommandLineArgument argument, object? value) /// 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) { if (value == null) @@ -67,6 +75,13 @@ public override string GetErrorMessage(CommandLineArgument argument, object? val } /// + /// + /// + /// 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 f283e403..5732846d 100644 --- a/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidatePatternAttribute.cs @@ -1,22 +1,24 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.RegularExpressions; namespace Ookii.CommandLine.Validation; /// -/// Validates that an argument's value matches the specified . +/// Validates that an argument's value matches the specified regular expression. /// /// /// -/// 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 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; @@ -35,9 +37,13 @@ public class ValidatePatternAttribute : ArgumentValidationAttribute /// is performed. /// ///
- public ValidatePatternAttribute(string pattern, RegexOptions options = RegexOptions.None) + public ValidatePatternAttribute( +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.Regex, nameof(options))] +#endif + string pattern, RegexOptions options = RegexOptions.None) { - _pattern = pattern; + _pattern = pattern ?? throw new ArgumentNullException(nameof(pattern)); _options = options; } @@ -67,15 +73,26 @@ public ValidatePatternAttribute(string pattern, RegexOptions options = RegexOpti /// {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 the pattern that values must match. + /// Gets the regular expression that values must match. /// /// /// The pattern that values must match. /// - public Regex Pattern => _patternRegex ??= new Regex(_pattern, _options); + public virtual 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. diff --git a/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs b/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs index 714bb0c0..e4b7e62a 100644 --- a/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateRangeAttribute.cs @@ -100,10 +100,24 @@ public override bool IsValid(CommandLineArgument argument, object? value) /// 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 acc738ee..3f707ec8 100644 --- a/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ValidateStringLengthAttribute.cs @@ -7,8 +7,8 @@ namespace Ookii.CommandLine.Validation; /// /// /// -/// 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 uses the raw string value provided by the user, before type conversion takes +/// place. /// /// /// @@ -81,10 +81,24 @@ public override bool IsValid(CommandLineArgument argument, object? value) /// 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); /// + /// + /// + /// 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 328a08be..a62679cf 100644 --- a/src/Ookii.CommandLine/Validation/ValidationMode.cs +++ b/src/Ookii.CommandLine/Validation/ValidationMode.cs @@ -1,7 +1,7 @@ namespace Ookii.CommandLine.Validation; /// -/// Specifies when a derived class of the class +/// Specifies when a class that derives from the class /// will run validation. /// public enum ValidationMode From 445a18b6d36a3e061dbbb5c0090088e4c0a41b7d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 5 Jul 2023 17:25:51 -0700 Subject: [PATCH 203/234] Remove index overloads. --- src/Ookii.CommandLine.Tests/SubCommandTest.cs | 4 +- src/Ookii.CommandLine/CommandLineArgument.cs | 6 +- .../CommandLineArgumentAttribute.cs | 2 +- src/Ookii.CommandLine/CommandLineParser.cs | 115 +++++------------- .../CommandLineParserGeneric.cs | 12 +- src/Ookii.CommandLine/Commands/CommandInfo.cs | 70 ----------- .../Commands/CommandManager.cs | 91 +++++--------- .../Validation/ArgumentValidationAttribute.cs | 2 +- .../Validation/ClassValidationAttribute.cs | 2 +- 9 files changed, 74 insertions(+), 230 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/SubCommandTest.cs b/src/Ookii.CommandLine.Tests/SubCommandTest.cs index a9e747eb..200df9fb 100644 --- a/src/Ookii.CommandLine.Tests/SubCommandTest.cs +++ b/src/Ookii.CommandLine.Tests/SubCommandTest.cs @@ -103,7 +103,7 @@ public void CreateCommandTest(ProviderKind kind) }; var manager = CreateManager(kind, options); - var command = (TestCommand?)manager.CreateCommand("test", new[] { "-Argument", "Foo" }, 0); + var command = (TestCommand?)manager.CreateCommand("test", new[] { "-Argument", "Foo" }); Assert.IsNotNull(command); Assert.AreEqual(ParseStatus.Success, manager.ParseResult.Status); Assert.AreEqual("Foo", command.Argument); @@ -115,7 +115,7 @@ public void CreateCommandTest(ProviderKind kind) Assert.AreEqual("Bar", command.Argument); Assert.AreEqual("", writer.BaseWriter.ToString()); - var command2 = (AnotherSimpleCommand?)manager.CreateCommand("anothersimplecommand", new[] { "skip", "-Value", "42" }, 1); + 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); diff --git a/src/Ookii.CommandLine/CommandLineArgument.cs b/src/Ookii.CommandLine/CommandLineArgument.cs index c081fc83..fe1df5fa 100644 --- a/src/Ookii.CommandLine/CommandLineArgument.cs +++ b/src/Ookii.CommandLine/CommandLineArgument.cs @@ -841,7 +841,7 @@ public string Description public DictionaryArgumentInfo? DictionaryInfo { get; } /// - /// Gets the value that the argument was set to in the last call to . + /// 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. @@ -849,7 +849,7 @@ public string Description /// /// /// The property provides an alternative method for accessing supplied argument - /// values, in addition to using the object returned by . + /// values, in addition to using the object returned by . /// /// /// If an argument was supplied on the command line, the property will equal the @@ -874,7 +874,7 @@ public string Description /// /// Gets a value indicating whether the value of this argument was supplied on the command line in the last - /// call to . + /// call to . /// /// /// if this argument's value was supplied on the command line when the arguments were parsed; otherwise, . diff --git a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs index 8c8eb681..e5e9e808 100644 --- a/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs +++ b/src/Ookii.CommandLine/CommandLineArgumentAttribute.cs @@ -45,7 +45,7 @@ namespace Ookii.CommandLine; /// 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 +/// method or the /// class. Instead, it must be requested using by setting the /// property to in the /// target method. diff --git a/src/Ookii.CommandLine/CommandLineParser.cs b/src/Ookii.CommandLine/CommandLineParser.cs index e73e7eaf..8cceac9f 100644 --- a/src/Ookii.CommandLine/CommandLineParser.cs +++ b/src/Ookii.CommandLine/CommandLineParser.cs @@ -530,7 +530,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - /// If the property is , a is thrown by the + /// If the property is , a is thrown by the /// method if an argument's value is supplied more than once. /// /// @@ -614,7 +614,7 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null public ImmutableArray NameValueSeparators => _nameValueSeparators; /// - /// Gets or sets a value that indicates whether usage help should be displayed if the + /// Gets or sets a value that indicates whether usage help should be displayed if the /// method returned . /// /// @@ -622,15 +622,15 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null /// /// /// - /// Check this property after calling the method or one + /// 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 + /// This property will always be if the /// method returned a non- value. /// /// - /// This property will always be if the + /// This property will always be if the /// method threw a , or if an argument used /// with the /// property or the event. @@ -884,7 +884,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = public object? Parse() { // GetCommandLineArgs include the executable, so skip it. - return Parse(Environment.GetCommandLineArgs(), 1); + return Parse(Environment.GetCommandLineArgs().AsMemory(1)); } /// @@ -892,47 +892,38 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// Parses the specified command line arguments. /// /// The command line arguments. - public object? Parse(ReadOnlyMemory args) + /// + /// is . + /// + public object? Parse(string[] args) { - int index = -1; - try - { - HelpRequested = false; - return ParseCore(args, ref index); - } - catch (CommandLineArgumentException ex) + if (args == null) { - HelpRequested = true; - ParseResult = ParseResult.FromException(ex, args.Slice(index)); - throw; + throw new ArgumentNullException(nameof(args)); } + + return Parse(args.AsMemory()); } /// /// - /// Parses the specified command line arguments, starting at the specified index. + /// Parses the specified command line arguments. /// /// 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) + public object? Parse(ReadOnlyMemory args) { - if (args == null) + int index = -1; + try { - throw new ArgumentNullException(nameof(index)); + HelpRequested = false; + return ParseCore(args, ref index); } - - if (index < 0 || index > args.Length) + catch (CommandLineArgumentException ex) { - throw new ArgumentOutOfRangeException(nameof(index)); + HelpRequested = true; + ParseResult = ParseResult.FromException(ex, args.Slice(index)); + throw; } - - return Parse(args.AsMemory(index)); } /// @@ -968,30 +959,21 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = /// /// - /// Parses the specified command line arguments, starting at the specified index, and - /// displays error messages and usage help if required. + /// Parses the specified command line arguments 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) + public object? ParseWithErrorHandling(string[] args) { if (args == null) { - throw new ArgumentNullException(nameof(index)); + throw new ArgumentNullException(nameof(args)); } - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - return ParseWithErrorHandling(args.AsMemory(index)); + return ParseWithErrorHandling(args.AsMemory()); } /// @@ -1110,44 +1092,7 @@ public string GetUsage(UsageWriter? usageWriter = null, int maximumLineLength = } /// - /// 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 . - /// - /// - /// - /// - /// - /// - /// -#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, int index, ParseOptions? options = null) - where T : class - { - var parser = new CommandLineParser(options); - return parser.ParseWithErrorHandling(args, index); - } - - /// - /// Parses the specified command line arguments, starting at the specified index, using the - /// type . + /// Parses the specified command line arguments using the type . /// /// The type defining the command line arguments. /// The command line arguments. diff --git a/src/Ookii.CommandLine/CommandLineParserGeneric.cs b/src/Ookii.CommandLine/CommandLineParserGeneric.cs index b20ccd2b..6836bfe8 100644 --- a/src/Ookii.CommandLine/CommandLineParserGeneric.cs +++ b/src/Ookii.CommandLine/CommandLineParserGeneric.cs @@ -98,10 +98,10 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null return (T?)base.Parse(); } - /// - public new T? Parse(string[] args, int index = 0) + /// + public new T? Parse(string[] args) { - return (T?)base.Parse(args, index); + return (T?)base.Parse(args); } /// @@ -116,10 +116,10 @@ public CommandLineParser(ArgumentProvider provider, ParseOptions? options = null return (T?)base.ParseWithErrorHandling(); } - /// - public new T? ParseWithErrorHandling(string[] args, int index = 0) + /// + public new T? ParseWithErrorHandling(string[] args) { - return (T?)base.ParseWithErrorHandling(args, index); + return (T?)base.ParseWithErrorHandling(args); } /// diff --git a/src/Ookii.CommandLine/Commands/CommandInfo.cs b/src/Ookii.CommandLine/Commands/CommandInfo.cs index 6807fefb..626ae179 100644 --- a/src/Ookii.CommandLine/Commands/CommandInfo.cs +++ b/src/Ookii.CommandLine/Commands/CommandInfo.cs @@ -150,33 +150,6 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) /// public Type? ParentCommandType { get; } - /// - /// Creates an instance of the command type parsing the specified arguments. - /// - /// 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. - /// - /// - /// - /// 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. - /// - /// - /// - /// is . - /// - /// - /// does not fall inside the bounds of . - /// - public ICommand? CreateInstance(string[] args, int index) - => CreateInstance(args.AsMemory(index)); - /// /// Creates an instance of the command type parsing the specified arguments. /// @@ -200,49 +173,6 @@ internal CommandInfo(Type commandType, string name, CommandManager manager) return command; } - - /// - /// 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. - /// 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. - /// - /// - /// - /// 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. - /// - /// - /// - /// is . - /// - /// does not fall inside the bounds of . - public (ICommand?, ParseResult) CreateInstanceWithResult(string[] args, int index) - { - if (args == null) - { - throw new ArgumentNullException(nameof(index)); - } - - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - return CreateInstanceWithResult(args.AsMemory(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. diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index 274bc9f7..b07b3b2b 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -469,62 +469,41 @@ public IEnumerable GetCommands() /// /// The name of the command. /// The arguments to the command. - /// The index in at which to start parsing the arguments. /// /// is . /// - /// - /// does not fall within the bounds of . - /// - public ICommand? CreateCommand(string? commandName, string[] args, int index) + public ICommand? CreateCommand(string? commandName, string[] args) { if (args == null) { throw new ArgumentNullException(nameof(args)); } - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - return CreateCommand(commandName, args.AsMemory(index)); + return CreateCommand(commandName, 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 (starting at ) - /// is the command name and the remaining ones are arguments for the command. - /// - /// - /// The index in at which to start parsing the arguments. + /// The command line arguments, where the first argument is the command name and the remaining + /// ones are arguments for the command. /// /// /// is . /// - /// - /// does not fall within the bounds of . - /// - public ICommand? CreateCommand(string[] args, int index = 0) + public ICommand? CreateCommand(string[] args) { if (args == null) { throw new ArgumentNullException(nameof(args)); } - if (index < 0 || index > args.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - return CreateCommand(args.AsMemory(index)); + return CreateCommand(args.AsMemory()); } - /// /// /// Finds and instantiates the subcommand with the name from the first argument, or if that @@ -560,7 +539,7 @@ public IEnumerable GetCommands() public ICommand? CreateCommand() { // Skip the first argument, it's the application name. - return CreateCommand(Environment.GetCommandLineArgs(), 1); + return CreateCommand(Environment.GetCommandLineArgs().AsMemory(1)); } @@ -570,7 +549,6 @@ public IEnumerable GetCommands() /// /// 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. @@ -578,12 +556,9 @@ public IEnumerable GetCommands() /// /// is /// - /// - /// does not fall inside the bounds of . - /// /// /// - /// This function creates the command by invoking the + /// This function creates the command by invoking the /// method, and then invokes the method on the command. /// /// @@ -598,9 +573,9 @@ public IEnumerable GetCommands() /// included. /// /// - public int? RunCommand(string? commandName, string[] args, int index) + public int? RunCommand(string? commandName, string[] args) { - var command = CreateCommand(commandName, args, index); + var command = CreateCommand(commandName, args); return command?.Run(); } @@ -669,21 +644,18 @@ public IEnumerable GetCommands() 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 (starting at ) - /// is the command name and the remaining ones are arguments for the command. - /// - /// - /// The index in at which to start parsing the arguments. + /// 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 + /// This function creates the command by invoking the /// method, and then invokes the method on the command. /// /// @@ -698,9 +670,9 @@ public IEnumerable GetCommands() /// included. /// /// - public int? RunCommand(string[] args, int index = 0) + public int? RunCommand(string[] args) { - var command = CreateCommand(args, index); + var command = CreateCommand(args); return command?.Run(); } @@ -710,7 +682,7 @@ public IEnumerable GetCommands() /// If it fails, writes error and usage information. /// /// - /// + /// /// /// /// @@ -739,7 +711,7 @@ public IEnumerable GetCommands() public int? RunCommand() { // Skip the first argument, it's the application name. - return RunCommand(Environment.GetCommandLineArgs(), 1); + return RunCommand(Environment.GetCommandLineArgs().AsMemory(1)); } /// @@ -789,7 +761,7 @@ public IEnumerable GetCommands() 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. @@ -801,7 +773,7 @@ public IEnumerable GetCommands() /// /// /// - /// This function creates the command by invoking the + /// 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. @@ -825,9 +797,9 @@ public IEnumerable GetCommands() /// whether the version command is included. /// /// - public async Task RunCommandAsync(string? commandName, string[] args, int index) + public async Task RunCommandAsync(string? commandName, string[] args) { - var command = CreateCommand(commandName, args, index); + var command = CreateCommand(commandName, args); if (command is IAsyncCommand asyncCommand) { return await asyncCommand.RunAsync(); @@ -882,21 +854,18 @@ public IEnumerable GetCommands() 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 (starting at ) - /// is the command name and the remaining ones are arguments for the command. - /// - /// - /// The index in at which to start parsing the arguments. + /// 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 + /// 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. @@ -920,9 +889,9 @@ public IEnumerable GetCommands() /// whether the version command is included. /// /// - public async Task RunCommandAsync(string[] args, int index = 0) + public async Task RunCommandAsync(string[] args) { - var command = CreateCommand(args, index); + var command = CreateCommand(args); if (command is IAsyncCommand asyncCommand) { return await asyncCommand.RunAsync(); @@ -937,7 +906,7 @@ public IEnumerable GetCommands() /// asynchronously. If it fails, writes error and usage information. /// /// - /// + /// /// /// /// diff --git a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs index 53a73d5e..6493f63a 100644 --- a/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ArgumentValidationAttribute.cs @@ -15,7 +15,7 @@ namespace Ookii.CommandLine.Validation; /// If validation fails, the validator will throw a /// with the category specified in the property. The /// method, the -/// method, +/// method, /// the generated , /// and the class will automatically display the error message and /// usage help if validation failed. diff --git a/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs b/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs index c46f7af8..17412a08 100644 --- a/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs +++ b/src/Ookii.CommandLine/Validation/ClassValidationAttribute.cs @@ -17,7 +17,7 @@ namespace Ookii.CommandLine.Validation; /// If validation fails, the validator will throw a /// with the category specified in the property. The /// method, the -/// method, +/// method, /// the generated , /// and the class will automatically display the error message and /// usage help if validation failed. From 3863a1095737f9dfb4700cc82bac0a5db16bbad5 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 5 Jul 2023 17:56:15 -0700 Subject: [PATCH 204/234] Updated migration guide. --- docs/Migrating.md | 28 ++++++++++++++++++++++++---- docs/refs.json | 6 ++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/Migrating.md b/docs/Migrating.md index 0ef91baa..64b65fa4 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -38,6 +38,10 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ - `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 @@ -53,13 +57,20 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`ParseOptions.NameValueSeparators`][]. - Properties that previously returned a [`ReadOnlyCollection`][] now return an [`ImmutableArray`][]. -- The `CommandLineArgumentAttribute.AllowsDuplicateDictionaryKeys` property was renamed to - [`AllowDuplicateDictionaryKeys`][] for consistency. +- 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. +- The default value of [`ParseOptions.ShowUsageOnError`][] has changed to [`UsageHelpRequest.SyntaxOnly`][]. ## Breaking API changes from version 2.4 @@ -120,7 +131,6 @@ 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. -[`AllowDuplicateDictionaryKeys`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgument_AllowDuplicateDictionaryKeys.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 @@ -129,13 +139,21 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`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 @@ -164,9 +182,11 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`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 [`TypeConverterArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter_1.htm [`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 diff --git a/docs/refs.json b/docs/refs.json index 1fb9729b..f9c916f7 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -47,7 +47,10 @@ "CommandInfo": "T_Ookii_CommandLine_Commands_CommandInfo", "CommandLineArgument": "T_Ookii_CommandLine_CommandLineArgument", "CommandLineArgument.AllowNull": "P_Ookii_CommandLine_CommandLineArgument_AllowNull", + "CommandLineArgument.DictionaryInfo": "P_Ookii_CommandLine_CommandLineArgument_DictionaryInfo", "CommandLineArgument.ElementType": "P_Ookii_CommandLine_CommandLineArgument_ElementType", + "CommandLineArgument.Kind": "P_Ookii_CommandLine_CommandLineArgument_Kind", + "CommandLineArgument.MultiValueInfo": "P_Ookii_CommandLine_CommandLineArgument_MultiValueInfo", "CommandLineArgumentAttribute": "T_Ookii_CommandLine_CommandLineArgumentAttribute", "CommandLineArgumentAttribute.CancelParsing": "P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing", "CommandLineArgumentAttribute.DefaultValue": "P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue", @@ -72,13 +75,16 @@ ], "CommandLineParser.Parse()": "M_Ookii_CommandLine_CommandLineParser_Parse__1", "CommandLineParser.ParseResult": "P_Ookii_CommandLine_CommandLineParser_ParseResult", + "CommandLineParser.ParseWithErrorHandling()": "Overload_Ookii_CommandLine_CommandLineParser_ParseWithErrorHandling", "CommandLineParser.WriteUsage()": "M_Ookii_CommandLine_CommandLineParser_WriteUsage", "CommandLineParser": "T_Ookii_CommandLine_CommandLineParser_1", "CommandLineParser.Parse()": "Overload_Ookii_CommandLine_CommandLineParser_1_Parse", "CommandLineParser.ParseWithErrorHandling()": "M_Ookii_CommandLine_CommandLineParser_1_ParseWithErrorHandling", "CommandManager": "T_Ookii_CommandLine_Commands_CommandManager", + "CommandManager.CreateCommand()": "Overload_Ookii_CommandLine_Commands_CommandManager_CreateCommand", "CommandManager.GetCommand()": "M_Ookii_CommandLine_Commands_CommandManager_GetCommand", "CommandManager.ParseResult": "P_Ookii_CommandLine_Commands_CommandManager_ParseResult", + "CommandManager.RunCommand()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommand", "CommandManager.RunCommandAsync()": "Overload_Ookii_CommandLine_Commands_CommandManager_RunCommandAsync", "CommandManagerAttribute": "!UNKNOWN!", "CommandNameComparison": "P_Ookii_CommandLine_Commands_CommandOptions_CommandNameComparison", From 5e5e8c9d689c6247011eb6791fafcc03e894edd0 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Wed, 5 Jul 2023 18:26:20 -0700 Subject: [PATCH 205/234] Documentation proofreading. --- README.md | 12 +++++------- docs/ChangeLog.md | 4 ++++ docs/Migrating.md | 17 +++++++++++------ docs/README.md | 8 ++++---- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b229dcd0..f05cc21f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ parse the supplied arguments for those values. In addition, you can generate usa displayed to the user. Ookii.CommandLine can be used with any kind of .Net application, whether console or GUI. Some -functionality, such as creating usage help, are primarily designed for console applications, but +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 @@ -51,7 +51,7 @@ partial class MyArguments [CommandLineArgument(IsPositional = true)] [Description("An optional positional argument.")] - public required int Optional { get; set; } = 42; + public int Optional { get; set; } = 42; [CommandLineArgument] [Description("An argument that can only be supplied by name.")] @@ -76,7 +76,7 @@ var arguments = MyArguments.Parse(); The `Parse()` method is added to the class through [source generation](docs/SourceGeneration.md). -This code will take the arguments from `Environment.GetCommandLineArgs()` (you can also manually +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. @@ -145,9 +145,10 @@ The .Net Standard 2.1 and .Net 6.0 and 7.0 versions utilize the framework `ReadO 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 7.0 version has additional support for `required` properties, and can utilize -`ISpanParsable` and `IParsable` for argument conversion. +`ISpanParsable` and `IParsable` for argument value conversions. ## Building and testing @@ -184,9 +185,6 @@ Ookii.CommandLine has a very different design. It uses a declarative approach to line arguments, using properties and attributes, which I personally prefer to the fluent API used by System.CommandLine, as it reduces the amount of code you typically need to write. -Additionally, Ookii.CommandLine is highly configurable, and supports a more PowerShell-like syntax, -as well as the POSIX-like syntax that System.CommandLine uses. - In the end, it comes down to personal preference. You should use whichever one suits your needs and coding style best. diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index a301b5bd..b7227bf8 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -23,6 +23,8 @@ please check the [migration guide](Migrating.md). - 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. @@ -225,6 +227,8 @@ may require substantial code changes and may change how command lines are parsed [`Environment.GetCommandLineArgs()`]: https://learn.microsoft.com/dotnet/api/system.environment.getcommandlineargs [`GeneratedCommandManagerAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_GeneratedCommandManagerAttribute.htm [`GeneratedParserAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_GeneratedParserAttribute.htm +[`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 diff --git a/docs/Migrating.md b/docs/Migrating.md index 64b65fa4..37752194 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -4,7 +4,8 @@ Ookii.CommandLine 4.0 and later have a number of breaking changes from version 3 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 @@ -14,9 +15,11 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ ## Breaking API changes from version 3.0 +- It's strongly recommended to use [source generation](SourceGeneration.md) unless you cannot meet + the requirements. - 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 localized resource. + 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 @@ -31,10 +34,6 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`KeyConverterAttribute`][] and [`ValueConverterAttribute`][] respectively - Constructor parameters can no longer be used to define command line arguments. Instead, all arguments must be defined using properties. -- 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. - `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`][]. @@ -71,6 +70,12 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ - 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 diff --git a/docs/README.md b/docs/README.md index c95fca59..abe2aaf5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,9 +1,9 @@ # Introduction -Ookii.CommandLine is a library that helps you with parsing the command line arguments for your -applications. In this guide, we will introduce the basic functionality using a tutorial, describe in -detail the command line parsing rules used and how to create various types of arguments, how to use -and customize usage help, and explain other functionality such as subcommands. +Ookii.CommandLine is a library for parsing the command line arguments for your applications. In this +guide, we will introduce the basic functionality using a tutorial, describe in detail the command +line parsing rules used and how to create various types of arguments, how to use and customize usage +help, and explain other functionality such as subcommands. In addition to this documentation, several [samples](../src/Samples) are provided, all with explanations of what they do and examples of their output. From 4618cfd47d9adce6322cc0156a841339c6f8e0be Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 09:52:11 -0700 Subject: [PATCH 206/234] Use LibraryImportAttribute for .Net 7 --- src/Ookii.CommandLine/NativeMethods.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Ookii.CommandLine/NativeMethods.cs b/src/Ookii.CommandLine/NativeMethods.cs index 32773241..747dce80 100644 --- a/src/Ookii.CommandLine/NativeMethods.cs +++ b/src/Ookii.CommandLine/NativeMethods.cs @@ -4,7 +4,7 @@ namespace Ookii.CommandLine; -static class NativeMethods +static partial class NativeMethods { static readonly IntPtr INVALID_HANDLE_VALUE = new(-1); @@ -62,6 +62,18 @@ public static IntPtr GetStandardHandle(StandardStream 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); @@ -70,6 +82,7 @@ public static IntPtr GetStandardHandle(StandardStream stream) [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr GetStdHandle(StandardHandle nStdHandle); +#endif [Flags] public enum ConsoleModes : uint From 15c86b19d60018403c3ef1fa7b63205b393b2aa9 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 09:54:17 -0700 Subject: [PATCH 207/234] Address code analysis warnings. --- src/Ookii.CommandLine/IParser.cs | 24 +++++++++++++++++++ src/Ookii.CommandLine/IParserProvider.cs | 6 +++++ src/Ookii.CommandLine/NativeMethods.cs | 2 ++ .../Ookii.CommandLine.csproj | 1 + src/Ookii.CommandLine/RingBuffer.Async.cs | 2 +- src/Ookii.CommandLine/RingBuffer.cs | 2 +- 6 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine/IParser.cs b/src/Ookii.CommandLine/IParser.cs index b9542fbe..9b4bdc2a 100644 --- a/src/Ookii.CommandLine/IParser.cs +++ b/src/Ookii.CommandLine/IParser.cs @@ -42,6 +42,18 @@ public interface IParser : IParserProvider /// error occurred, or argument parsing was canceled by the /// property or a method argument that returned . /// + /// + /// 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); @@ -59,6 +71,18 @@ public interface IParser : IParserProvider /// error occurred, or argument parsing was canceled by the /// property or a method argument that returned . /// + /// + /// 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); diff --git a/src/Ookii.CommandLine/IParserProvider.cs b/src/Ookii.CommandLine/IParserProvider.cs index c9a98fd1..4f759cf4 100644 --- a/src/Ookii.CommandLine/IParserProvider.cs +++ b/src/Ookii.CommandLine/IParserProvider.cs @@ -44,6 +44,12 @@ public interface IParserProvider /// 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); } diff --git a/src/Ookii.CommandLine/NativeMethods.cs b/src/Ookii.CommandLine/NativeMethods.cs index 747dce80..50b02b76 100644 --- a/src/Ookii.CommandLine/NativeMethods.cs +++ b/src/Ookii.CommandLine/NativeMethods.cs @@ -97,11 +97,13 @@ public enum ConsoleModes : uint 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 diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index caf35a26..c73f710a 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -28,6 +28,7 @@ icon.png true true + en-US diff --git a/src/Ookii.CommandLine/RingBuffer.Async.cs b/src/Ookii.CommandLine/RingBuffer.Async.cs index 9edf14a8..dc9ca7b6 100644 --- a/src/Ookii.CommandLine/RingBuffer.Async.cs +++ b/src/Ookii.CommandLine/RingBuffer.Async.cs @@ -38,7 +38,7 @@ public async Task WriteToAsync(TextWriter writer, int length, CancellationToken } } - private async Task WriteAsyncHelper(TextWriter writer, char[] buffer, int index, int length, CancellationToken cancellationToken) + 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); diff --git a/src/Ookii.CommandLine/RingBuffer.cs b/src/Ookii.CommandLine/RingBuffer.cs index fc167e39..d9ff7928 100644 --- a/src/Ookii.CommandLine/RingBuffer.cs +++ b/src/Ookii.CommandLine/RingBuffer.cs @@ -195,5 +195,5 @@ private void Resize(int capacityNeeded) _buffer = newBuffer; } - private partial void WriteHelper(TextWriter writer, char[] buffer, int index, int length); + private static partial void WriteHelper(TextWriter writer, char[] buffer, int index, int length); } From fa6a6fddd6cd3ce0429a6eeb85e201ea0d245cd0 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 13:22:26 -0700 Subject: [PATCH 208/234] Disable inheritance for generation attributes. --- docs/Arguments.md | 112 +++++++++--------- docs/DefiningArguments.md | 63 ++++++---- docs/ParsingArguments.md | 14 +-- docs/Subcommands.md | 11 +- docs/Tutorial.md | 34 +++--- docs/UsageHelp.md | 11 +- docs/Utilities.md | 42 ++++++- docs/Validation.md | 81 ++++++++----- .../GeneratedCommandManagerAttribute.cs | 2 +- .../GeneratedParserAttribute.cs | 2 +- 10 files changed, 225 insertions(+), 147 deletions(-) diff --git a/docs/Arguments.md b/docs/Arguments.md index 800511d2..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 @@ -57,9 +56,25 @@ argument values. For example, `-ArgumentName:foo:bar` will give `-ArgumentName` 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 @@ -91,8 +106,8 @@ Now, consider the following invocation: value1 -NamedOnly value2 value3 ``` -In this case, "value1" is not preceded by a name; therefore, it is matched to `-Positional1` -argument. The value "value2" follows a name, so it is matched to the argument with the name +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`. @@ -148,6 +163,24 @@ You must use the name/value separator (a colon or equals sign by default) to spe 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 +``` + +This only works for switch arguments, and does not apply to long names or the default parsing mode. + ## Arguments with multiple values Some arguments can take multiple values; these are _multi-value arguments_. These arguments can be @@ -320,24 +353,27 @@ change this using the [`ParseOptions.Culture`][] property, but be very careful i ## 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 [`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]. -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. - 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 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 @@ -355,50 +391,14 @@ place of the regular argument name, and an additional single-character short nam 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. For information on how to set these options, [see here](DefiningArguments.md#longshort-mode). -When using long/short mode, 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. The short name doesn't have to -use the first letter of the long name; it can be anything. - -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 outlined above, -with all the same options. - ## More information Next, let's take a look at how to [define arguments](DefiningArguments.md). diff --git a/docs/DefiningArguments.md b/docs/DefiningArguments.md index c1a8411d..425de30e 100644 --- a/docs/DefiningArguments.md +++ b/docs/DefiningArguments.md @@ -16,8 +16,8 @@ partial class Arguments } ``` -This enables the use of [source generation](SourceGeneration.md), which has several advantages and -should be used unless you cannot meet the requirements. +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 @@ -96,7 +96,7 @@ 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: @@ -124,6 +124,19 @@ 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 @@ -138,7 +151,7 @@ public required string SomeArgument { get; set; } public int OtherArgument { get; set; } ``` -Now, both `-SomeArgument` and `-OtherArgument` are required and positional. +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 @@ -168,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] @@ -189,13 +202,14 @@ the list after parsing has completed. public List AlsoMultiValue { get; } = new(); ``` -If you are _not_ using source generation, 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 +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 is both a switch and a multi-value argument. +A multi-value argument whose type is a boolean or a nullable boolean is both a switch and a +multi-value argument. ```csharp [CommandLineArgument] @@ -223,9 +237,9 @@ public Dictionary? Dictionary { get; set; } public SortedDictionary AlsoDictionary { get; } = new(); ``` -As above, when using a read-only property when not using source generation, you should use either -[`Dictionary`][] or [`IDictionary`][] as the type of the property, -otherwise the nullability of the value type cannot be determined.. +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 @@ -242,8 +256,8 @@ 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, 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. +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. @@ -255,7 +269,7 @@ Alternatively, you can specify the default value using the public int SomeArgument { get; set; } ``` -The [`DefaultValue`] property must be either the type of the argument, or a string that can be +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. @@ -268,7 +282,8 @@ The value of the [`CommandLineArgumentAttribute.DefaultValue`][] property will b argument's description in the [usage help](UsageHelp.md). In this case, it will be included regardless of whether you are using source generation. -Default values will be ignored if specified for a required argument. +Default values will be ignored if specified for a required argument or a multi-value or dictionary +argument. ### Argument descriptions @@ -345,8 +360,8 @@ as a base class to adapt it. ### Arguments that cancel parsing -You can indicate that argument parsing should stop immediately return when an argument is supplied -by setting the [`CommandLineArgumentAttribute.CancelParsing`][] property. +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 to [`CancelMode.Abort`][], parsing is stopped when the argument is encountered. The rest of the command line is not processed, and @@ -354,7 +369,8 @@ encountered. The rest of the command line is not processed, and [`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 = CancelMode.Abort)] @@ -450,7 +466,6 @@ only `-`. [ParseOptions(ArgumentNamesPrefixes = new[] { '-' })] partial class Arguments { - } ``` @@ -604,9 +619,9 @@ 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) @@ -629,8 +644,10 @@ Next, we'll take a look at how to [parse the arguments we've defined](ParsingArg [`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 diff --git a/docs/ParsingArguments.md b/docs/ParsingArguments.md index a93003e6..a547bdde 100644 --- a/docs/ParsingArguments.md +++ b/docs/ParsingArguments.md @@ -59,7 +59,7 @@ The generated [`Parse()`][Parse()_7] methods and the static [`Parse()`][Parse a [`CommandLineArgumentException`][]. They can throw other exceptions if the arguments type violates one of the rules for valid arguments (such as defining an optional positional argument after a required one). An exception from this method typically indicates a mistake in your arguments class. When -using source generation, these kinds of errors are typically caught at compile time. +using source generation, these kinds of errors are often caught at compile time. You can customize various aspects of the parsing behavior using either the [`ParseOptionsAttribute`][], applied to your arguments class, or a [`ParseOptions`][] instance @@ -78,7 +78,7 @@ var options = new ParseOptions() UsageWriter = new UsageWriter(writer); }; -var arguments = CommandLineParser.Parse(options); +var arguments = MyArguments.Parse(options); if (arguments == null) { // There are probably better ways to show help in a GUI app than this. @@ -104,11 +104,11 @@ and create your own error message. ## Manual parsing and error handling -The static [`Parse()`][Parse()_1] method and its overloads will likely be sufficient for most -use cases. However, sometimes you may want even more fine-grained control. This includes the ability -to handle the [`ArgumentParsed`][] and [`DuplicateArgument`][DuplicateArgument_0] events, and to get -additional information about the arguments using the [`Arguments`][Arguments_0] property or the -[`GetArgument`][] function. +The generated [`Parse()`][Parse()_7] methods and the static [`Parse()`][Parse()_1] method and +their overloads will likely be sufficient for most use cases. However, sometimes you may want even +more fine-grained control. This includes the ability to handle the [`ArgumentParsed`][] and +[`DuplicateArgument`][DuplicateArgument_0] events, and to get additional information about the +arguments using the [`Arguments`][Arguments_0] property or the [`GetArgument`][] function. In this case, you can manually create an instance of the [`CommandLineParser`][] class. Then, call the instance [`ParseWithErrorHandling()`][ParseWithErrorHandling()_1] or [`Parse()`][Parse()_5] method. diff --git a/docs/Subcommands.md b/docs/Subcommands.md index 109ec6f8..54c52c9f 100644 --- a/docs/Subcommands.md +++ b/docs/Subcommands.md @@ -70,8 +70,9 @@ name. Then, the [`ICommand.Run()`][] method will be called. All of the functionality and [options](#subcommand-options) available with regular arguments types are available with commands too, including [usage help generation](#subcommand-usage-help), -[long/short mode](Arguments.md#longshort-mode), all kinds of arguments, validators, source -generation, etc. +[long/short mode](Arguments.md#longshort-mode), [name transformation](#name-transformation), +[all kinds of arguments](DefiningArguments.md), [validators](Validation.md), +[source generation](SourceGeneration.md), etc. ### Name transformation @@ -104,8 +105,8 @@ value, the `ReadDirectoryCommand` class above will create a command named `read- ### Command aliases -Like argument names, a command can have one or more aliases, alternative names that can be used -to invoke the command. Simply apply the [`AliasAttribute`][] to the command class. +Like command line arguments, a command can have one or more aliases, alternative names that can be +used to invoke the command. Simply apply the [`AliasAttribute`][] to the command class. ```csharp [GeneratedParser] @@ -130,7 +131,7 @@ Automatic prefix aliases for command names can be disabled using the ### Asynchronous commands -It's possible to use asynchronous code with subcommands. To do this, implement the +It's possible to create subcommands that execute asynchronous code. To do this, implement the [`IAsyncCommand`][] interface, which derives from [`ICommand`][], and use the [`CommandManager.RunCommandAsync()`][] method (see [below](#using-subcommands)). diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 9c29878b..8f1e26ab 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -1,8 +1,8 @@ # Tutorial: getting started with Ookii.CommandLine -This tutorial will show you the basics of how to use Ookii.CommandLine. It will show you how to +This tutorial will show you the basics of how to use Ookii.CommandLine. It will demonstrate how to create an application that parses the command line and shows usage help, how to customize some of -the options—including the POSIX-like long/short mode—and how to use subcommands. +the options—including using POSIX conventions—and how to use subcommands. Refer to the [documentation](README.md) for more detailed information. @@ -42,7 +42,7 @@ partial class Arguments ``` If you are targeting a .Net version before .Net 7.0, the `required` keyword is not available. In -that case, use the following code instead: +that case, use the following code instead for the `Path` property: ```csharp [CommandLineArgument(IsPositional = true, IsRequired = true)] @@ -84,7 +84,7 @@ return 0; This code parses the arguments we defined, returns an error code if it was unsuccessful, and writes the contents of the file specified by the path argument to the console. -The important part is the call to `Arguments.Parse()`. This static method was created by the +The important part is the call to [`Arguments.Parse()`][]. This static method was created by the [`GeneratedParserAttribute`][], and will parse your arguments, handle and print any errors, and print usage help if required. @@ -190,7 +190,7 @@ tutorial 1.0.0 ``` By default, it shows the assembly's name and informational version. It'll also show the assembly's -copyright information, if there is any (there's not in this case). You can also use the +copyright information, if there is any (there's not in this case). You can use the [`AssemblyTitleAttribute`][] or [`ApplicationFriendlyNameAttribute`][] attribute to specify a custom name instead of the assembly name. @@ -241,9 +241,9 @@ public bool Inverted { get; set; } ``` This defines two new arguments. The first, `-MaxLines`, uses `int?` as its type, so it will only -accept integer numbers, and be null if not supplied. This argument is not positional (you must use -the name), and it's optional. We've also added a validator to ensure the value is positive, and -since `-MaxLines` might be a bit verbose, we've given it an alias `-Lines`, which can be used as an +accept integers, and be null if not supplied. This argument is not positional (you must use the +name), and it's optional. We've also added a validator to ensure the value is positive, and since +`-MaxLines` might be a bit verbose, we've given it an alias `-Lines`, which can be used as an alternative name to supply the argument. > An argument can have any number of aliases; just repeat the [`AliasAttribute`][] attribute. @@ -416,11 +416,12 @@ default (on Windows only, `/` is also accepted by default). You can make the arg sensitive. And there's more. One thing you may want to do is use POSIX-like conventions, instead of the default PowerShell-like -parsing behavior. With POSIX conventions, arguments have separate long and short, one-character -names, which use different prefixes (typically `--` for long names and `-` for short). Argument -names are typically lowercase, with dashes between words, and are case sensitive. These are the same -conventions followed by tools such as `dotnet` or `git`, and many others. For a cross-platform -application, you may prefer these conventions over the default, but it's up to you of course. +parsing behavior. With POSIX conventions, arguments can have a short, one-character name, and a +separate long name, which uses a different prefix (typically `--` is used for long names, and `-` +for short). Argument names are typically lowercase, with dashes between words, and are case +sensitive. These are the same conventions followed by tools such as `dotnet` or `git`, and many +others. For a cross-platform application, you may prefer these conventions over the default, but +it's up to you of course. A convenient way to change these options is to use the [`ParseOptionsAttribute`][], which you can apply to your class. Let's use it to enable POSIX mode: @@ -483,7 +484,7 @@ and the short name `-i`. Finally, `--path` only has a long name, and is still po these names are now case sensitive. > Name transformations don't apply to names or value descriptions that are explicitly specified, so -> we had to change "number" and "max" manually to match. +> we had to change "number" and the alias "lines" manually to match. Now, the usage help looks like this: @@ -737,8 +738,8 @@ argument, since that would be redundant with the `version` command. The usage help for the single arguments class would print an application description at the top, but the command list doesn't have anything like that. We can, however, add it. -To do, make the following change to the [`CommandOptions`][] (and add `using Ookii.CommandLine` at -the top of the file): +To do this, make the following change to the [`CommandOptions`][] (and add `using Ookii.CommandLine` +at the top of the file): ```csharp var options = new CommandOptions() @@ -975,6 +976,7 @@ following resources: [`AliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_AliasAttribute.htm [`ApplicationFriendlyNameAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ApplicationFriendlyNameAttribute.htm +[`Arguments.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_IParser_1_Parse.htm [`AssemblyTitleAttribute`]: https://learn.microsoft.com/dotnet/api/system.reflection.assemblytitleattribute [`AsyncCommandBase.Run()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_AsyncCommandBase_Run.htm [`AsyncCommandBase`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_AsyncCommandBase.htm diff --git a/docs/UsageHelp.md b/docs/UsageHelp.md index e2a9fc63..4f6aa2fd 100644 --- a/docs/UsageHelp.md +++ b/docs/UsageHelp.md @@ -11,7 +11,7 @@ If you use the generated [`Parse()`][Parse()_7] method (with [source generation] static [`CommandLineParser.Parse()`][] method, or the [`CommandLineParser.ParseWithErrorHandling()`][] method, usage help will be printed automatically in the event the command line is invalid, or the `-Help` argument was used. You can customize the -output using the [`ParseOptions.UsageWriter`][] property. +format using the [`ParseOptions.UsageWriter`][] property. If you don't use those methods, you can generate the usage help using the [`CommandLineParser.WriteUsage()`][] method. By default, the [`CommandLineParser.WriteUsage()`][] @@ -250,7 +250,7 @@ To hide an argument, use the [`CommandLineArgumentAttribute.IsHidden`][] propert public int Argument { get; set; } ``` -Note that positional arguments cannot be hidden. +Note that positional and required arguments cannot be hidden. ## Color output @@ -267,7 +267,7 @@ determine whether color is supported. Color will only be enabled if: [`UsageWriter`][] uses virtual terminal sequences to set color. Several components of the help have preset colors, which can be customized using properties of the [`UsageWriter`][] class. Set them to any of the constants of the [`TextFormat`][] class, or the return value of the [`GetExtendedColor()`][] method -for any 24-bit color, or any other valid virtual terminal sequence. +for any 24-bit color. In order to support proper white-space wrapping for text that contains virtual terminal sequences, the [`LineWrappingTextWriter`][] class will not count virtual terminal sequences as part of the line @@ -282,7 +282,8 @@ The below is an example of the usage help with the default colors. The usage help can be heavily customized. We've already seen how it can be customized using things such as custom value descriptions, or various properties of the [`UsageWriter`][] class. These can also be used to control the indentation of the text, what elements to include, and various small -formatting changes such as whether to white space or the custom name/value separator. +formatting changes such as whether to use white space or the custom separator between argument names +and values. To customize the usage even further, you can derive a class from the [`UsageWriter`][] class. The [`UsageWriter`][] class has protected virtual methods for every part of the usage. These range from @@ -332,7 +333,7 @@ OPTIONS: ``` The [WPF usage sample](../src/Samples/Wpf) is another example that uses a custom [`UsageWriter`][], in -this case to output the usage help as HTML. +this case to format the usage help as HTML. You can see that the [`UsageWriter`][] class offers a lot of flexibility to customize the usage help to your liking. diff --git a/docs/Utilities.md b/docs/Utilities.md index 1f8d416d..3bbffcc3 100644 --- a/docs/Utilities.md +++ b/docs/Utilities.md @@ -99,10 +99,10 @@ The [`VirtualTerminal`][] class allows you to determine whether virtual terminal supported, and to enable them. The [`UsageWriter`][] class uses this internally to enable color output when possible. -The [`TextFormat`][] class provides a number of constants for the predefined background and foreground -colors and formats supported by the console, as well as a method to create a VT sequence for any -24-bit color. These can be used to change the default usage help colors, or to apply color to your -own text. +The [`TextFormat`][] structure provides a number of values for the predefined background and +foreground colors and formats supported by the console, as well as a method to create a VT sequence +for any 24-bit color. These can be used to change the default usage help colors, or to apply color +to your own text by writing them to the console. For example, you can use the following to write in color when supported: @@ -128,6 +128,40 @@ and they return a disposable type that will revert the console mode when dispose collected. On other platforms, it only checks for support and disposing the returned instance does nothing. +In the [tutorial](Tutorial.md), we created an application with an `--inverted` argument, that +actually just set the console to use a white background and a black foreground, instead of truly +inverting the console colors. With virtual terminal support, we can update the `read` command to use +true inversion. + +```csharp +public int Run() +{ + using var support = VirtualTerminal.EnableColor(StandardStream.Output); + if (support.IsSupported && Inverted) + { + Console.Write(TextFormat.Negative); + } + + var lines = File.ReadLines(Path); + if (MaxLines is int maxLines) + { + lines = lines.Take(maxLines); + } + + foreach (var line in lines) + { + Console.WriteLine(line); + } + + if (support.IsSupported && Inverted) + { + Console.Write(TextFormat.Default); + } + + return 0; +} +``` + [`Console.WindowWidth`]: https://learn.microsoft.com/dotnet/api/system.console.windowwidth [`EnableColor()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_EnableColor.htm [`EnableVirtualTerminalSequences()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Terminal_VirtualTerminal_EnableVirtualTerminalSequences.htm diff --git a/docs/Validation.md b/docs/Validation.md index cf8e9b7f..599c442a 100644 --- a/docs/Validation.md +++ b/docs/Validation.md @@ -22,16 +22,16 @@ There are validators that check the value of an argument, and validators that ch inter-dependencies. The following are the built-in argument value validators (dependency validators are discussed [below](#argument-dependencies-and-restrictions)): -Validator | Description | Applied --------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------- -[`ValidateCountAttribute`][] | Validates that the number of items for a multi-value argument is in the specified range. | After parsing. -[`ValidateEnumValueAttribute`][] | Validates that the value is one of the defined values for an enumeration. The default [`TypeConverter`][] for an enumeration allows conversion from the underlying value, even if that value is not a defined value for the enumeration. This validator prevents that. See also [enumeration type conversion](Arguments.md#enumeration-conversion). | After conversion. -[`ValidateNotEmptyAttribute`][] | Validates that the value of an argument is not an empty string. | Before conversion. -[`ValidateNotNullAttribute`][] | Validates that the value of an argument is not null. This is only useful if the [`TypeConverter`][] for an argument can return null (for example, the [`NullableConverter`][] can). It's not necessary to use this validator on non-nullable value types, or if using .Net 6.0 or later, on non-nullable reference types. | After conversion. -[`ValidateNotWhiteSpaceAttribute`][] | Validates that the value of an argument is not an empty string or a string containing only white-space characters. | Before conversion. -[`ValidatePatternAttribute`][] | Validates that the value of an argument matches the specified regular expression. | Before conversion. -[`ValidateRangeAttribute`][] | Validates that the value of an argument is in the specified range. This can be used on any type that implements the [`IComparable`][] interface. | After conversion. -[`ValidateStringLengthAttribute`][] | Validates that the length of an argument's string value is in the specified range. | Before conversion. +Validator | Description | Applied +-------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------- +[`ValidateCountAttribute`][] | Validates that the number of items for a multi-value argument is in the specified range. | After parsing. +[`ValidateEnumValueAttribute`][] | Validates that the value is one of the defined values for an enumeration. The [`EnumConverter`][] class allows conversion from the underlying value, even if that value is not a defined value for the enumeration. This validator prevents that. See also [enumeration type conversion](Arguments.md#enumeration-conversion). | After conversion. +[`ValidateNotEmptyAttribute`][] | Validates that the value of an argument is not an empty string. | Before conversion. +[`ValidateNotNullAttribute`][] | Validates that the value of an argument is not null. This is only useful if the [`ArgumentConverter`][] for an argument can return null (for example, the [`NullableConverter`][] can). It's not necessary to use this validator on non-nullable value types, or if using .Net 6.0 or later, or [source generation](SourceGeneration.md), on non-nullable reference types. | After conversion. +[`ValidateNotWhiteSpaceAttribute`][] | Validates that the value of an argument is not an empty string or a string containing only white-space characters. | Before conversion. +[`ValidatePatternAttribute`][] | Validates that the value of an argument matches the specified regular expression. | Before conversion. +[`ValidateRangeAttribute`][] | Validates that the value of an argument is in the specified range. This can be used on any type that implements the [`IComparable`][] interface. | After conversion. +[`ValidateStringLengthAttribute`][] | Validates that the length of an argument's string value is in the specified range. | Before conversion. Note that there is no `ValidateSetAttribute`, or an equivalent way to make sure that an argument is one of a predefined set of values, because you're encouraged to use an enumeration type for this @@ -93,16 +93,15 @@ because it cannot know the purpose of the of the pattern used. Instead, it will error message stating the value is invalid. You can use the [`ValidatePatternAttribute.ErrorMessage`][] property to specify a custom error message. -The [`ValidateEnumValueAttribute`][] validator includes the possible enum values in the error +The [`ValidateEnumValueAttribute`][] validator includes the possible enumeration values in the error message by default. If there are a lot of values, you may wish to disable this, which can be done -with the [`ValidateEnumValueAttribute.IncludeValuesInErrorMessage`][] property. Note that this error -message is only used if validation failed, which only happens if a numeric value was used that -didn't match a defined value. This message is not shown if an invalid string value was used, as that -will fail at the point of conversion, before the validator is applied. +with the [`ValidateEnumValueAttribute.IncludeValuesInErrorMessage`][] property. This same error +message, with the values included, is used by the [`EnumConverter`][] class when the value is an +unrecognized name. -As with all other error messages, the messages for all built-in validators are obtained from the -[`LocalizedStringProvider`][] class and can be customized by deriving a custom string provider from -that class. +As with all other library error messages, the messages for all built-in validators are obtained from +the [`LocalizedStringProvider`][] class and can be customized by deriving a custom string provider +from that class. ### Usage help @@ -110,7 +109,7 @@ One benefit of using validators is that they can add a help message for their co usage help. For example, the [`ValidateRangeAttribute`][] will show a usage help message like "Must be between 0 and 100." These messages will be added to the end of the argument's description. -The only exceptions is the [`ValidatePatternAttribute`][], which does not know the intent of the +The only exceptions are the [`ValidatePatternAttribute`][], which does not know the intent of the pattern and can therefore not provide a meaningful help message to the user, and the [`ValidateNotNullAttribute`][]. In this case, you should manually add a message to the argument's description to make the intent clear. @@ -134,10 +133,10 @@ Besides argument value validators, there are also a number of built-in validator dependencies or restrictions on other arguments. The following validators are available: Validate | Description ----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [`ProhibitsAttribute`][] | Indicates that an argument cannot be used in combination with another argument. [`RequiresAttribute`][] | Indicates that an argument can only be used in combination with another argument. -[`RequiresAnyAttribute`][] | This is a class validator, that must be applied to the arguments class instead of an argument, which validates that at least one of the specified arguments is present on the command line. +[`RequiresAnyAttribute`][] | Validates that at least one of the specified arguments is present on the command line. This is a class validator, that must be applied to the arguments class instead of an argument. For example, you might have an application that can read data from a file, or from a server at a specified IP address and port. You could express these arguments as follows: @@ -147,7 +146,7 @@ specified IP address and port. You could express these arguments as follows: [RequiresAny(nameof(Path), nameof(Ip))] partial class ProgramArguments { - [CommandLineArgument(Position = 0)] + [CommandLineArgument(IsPositional = true)] [Description("The path to use.")] public FileInfo? Path { get; set; } @@ -156,13 +155,17 @@ partial class ProgramArguments [Prohibits(nameof(Path))] public IPAddress? Ip { get; set; } - [CommandLineArgument(DefaultValue = 80)] + [CommandLineArgument] [Description("The port to connect to.")] [Requires(nameof(Ip))] - public int Port { get; set; } + public int Port { get; set; } = 80; } ``` +:warning: **IMPORTANT:** The [`RequiresAttribute`][], [`ProhibitsAttribute`][] and +[`RequiresAnyAttribute`][] all take the name of an _argument_ as their parameters. The use of +`nameof()` as above is only safe if the member names match the argument names. + The `-Ip` argument uses the [`ProhibitsAttribute`][] to indicate it is mutually exclusive with the "Path" argument. The `-Port` argument uses the [`RequiresAttribute`][] to indicate it can only be used when the `-Ip` argument is also specified. @@ -176,9 +179,28 @@ Just like the argument value validators, the dependency validators will add a us desired. In the case of a class validator like the [`RequiresAnyAttribute`][], this message is shown before the description list. -:warning: **IMPORTANT:** The [`RequiresAttribute`][], [`ProhibitsAttribute`][] and -[`RequiresAnyAttribute`][] all take the name of an _argument_ as their parameters. The use of -`nameof()` as above is only safe if the member names match the argument names. +For example, the usage help of the above arguments looks like this: + +```text +Usage: cscoretest [[-Path] ] [-Help] [-Ip ] [-Port ] [-Version] + +You must use at least one of: -Path, -Ip. + + -Path + The path to use. + + -Help [] (-?, -h) + Displays this help message. + + -Ip + The IP address to connect to. Cannot be used with: -Path. + + -Port + The port to connect to. Must be used with: -Ip. Default value: 80. + + -Version [] + Displays version information. +``` Check out the [argument dependencies sample](../src/Samples/ArgumentDependencies/) to see this in action. @@ -258,6 +280,7 @@ does not apply to validators that don't use [`ValidationMode.BeforeConversion`][ Now that you know (almost) everything there is to know about arguments, let's move on to [subcommands](Subcommands.md). +[`ArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverter.htm [`ArgumentValidationAttribute.IsSpanValid`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsSpanValid.htm [`ArgumentValidationAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ArgumentValidationAttribute.htm [`ArgumentValidationWithHelpAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ArgumentValidationWithHelpAttribute.htm @@ -269,6 +292,7 @@ Now that you know (almost) everything there is to know about arguments, let's mo [`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 [`DateOnly`]: https://learn.microsoft.com/dotnet/api/system.dateonly +[`EnumConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_EnumConverter.htm [`ErrorCategory`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ArgumentValidationAttribute_ErrorCategory.htm [`GetErrorMessage()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetErrorMessage.htm [`GetUsageHelp()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_GetUsageHelp.htm @@ -276,13 +300,12 @@ Now that you know (almost) everything there is to know about arguments, let's mo [`IComparable`]: https://learn.microsoft.com/dotnet/api/system.icomparable-1 [`IsValid()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Validation_ArgumentValidationAttribute_IsValid.htm [`LocalizedStringProvider`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_LocalizedStringProvider.htm -[`NullableConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.nullableconverter +[`NullableConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_NullableConverter.htm [`Ookii.CommandLine.Validation`]: https://www.ookii.org/docs/commandline-4.0/html/N_Ookii_CommandLine_Validation.htm [`ProhibitsAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ProhibitsAttribute.htm [`ReadOnlySpan`]: https://learn.microsoft.com/dotnet/api/system.readonlyspan-1 [`RequiresAnyAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_RequiresAnyAttribute.htm [`RequiresAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_RequiresAttribute.htm -[`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter [`UsageWriter.IncludeValidatorsInDescription`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_UsageWriter_IncludeValidatorsInDescription.htm [`ValidateCountAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Validation_ValidateCountAttribute.htm [`ValidateEnumValueAttribute.IncludeValuesInErrorMessage`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Validation_ValidateEnumValueAttribute_IncludeValuesInErrorMessage.htm diff --git a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs index 6772967b..fe8ea97d 100644 --- a/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs +++ b/src/Ookii.CommandLine/Commands/GeneratedCommandManagerAttribute.cs @@ -25,7 +25,7 @@ namespace Ookii.CommandLine.Commands; /// /// /// Source generation -[AttributeUsage(AttributeTargets.Class)] +[AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class GeneratedCommandManagerAttribute : Attribute { /// diff --git a/src/Ookii.CommandLine/GeneratedParserAttribute.cs b/src/Ookii.CommandLine/GeneratedParserAttribute.cs index e35afc25..f3114084 100644 --- a/src/Ookii.CommandLine/GeneratedParserAttribute.cs +++ b/src/Ookii.CommandLine/GeneratedParserAttribute.cs @@ -30,7 +30,7 @@ namespace Ookii.CommandLine; /// /// /// Source generation -[AttributeUsage(AttributeTargets.Class)] +[AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class GeneratedParserAttribute : Attribute { /// From f409a3e4201913e64039b26f428a97a12d73aea7 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 14:31:40 -0700 Subject: [PATCH 209/234] More documentation proofreading. --- docs/SourceGeneration.md | 14 +++++++------- docs/SourceGenerationDiagnostics.md | 29 +++++++++++++---------------- docs/Subcommands.md | 10 ++++++---- docs/Utilities.md | 10 +++++----- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/docs/SourceGeneration.md b/docs/SourceGeneration.md index 204ddf1e..c4f4c4d6 100644 --- a/docs/SourceGeneration.md +++ b/docs/SourceGeneration.md @@ -29,8 +29,8 @@ Using source generation has several benefits: A few restrictions apply to projects that use Ookii.CommandLine's source generation: -- The project must be a C# project (other languages are not supported), using C# version 8 or later. - Other languages or older C# versions are not supported. +- The project must be a C# project, using C# version 8 or later. Other languages or older C# + versions are not supported. - The project must be built using using the .Net 6.0 SDK or a later version. - You can still target older runtimes supported by Ookii.CommandLine, down to .Net Framework 4.6, but you must build the project using the .Net 6.0 SDK or newer. @@ -187,7 +187,7 @@ partial class Arguments ``` When using a reflection-based parser, `Arg2` would have its value set to "foo" when omitted (since -Ookii.CommandLine doesn't assign the property if the argument is not specifies), but that default +Ookii.CommandLine doesn't assign the property if the argument is not specified), but that default value would not be included in the usage help, whereas the default value of `Arg1` will be. With the [`GeneratedParserAttribute`][], both `Arg1` and `Arg2` will have the default value of "foo" @@ -195,11 +195,11 @@ shown in the usage help, making the two forms identical. Additionally, `Arg2` co non-nullable because it was initialized to a non-null value, something which isn't possible for `Arg1` without initializing the property to a value that will not be used. -If both a property initializer and the [`DefaultValue`][DefaultValue_1] property are both used, the [`DefaultValue`][DefaultValue_1] -property takes precedence. +If both a property initializer and the [`DefaultValue`][DefaultValue_1] property are used, the +[`DefaultValue`][DefaultValue_1] property takes precedence. This only works if the property initializer is a literal, enumeration value, reference to a constant, -or a null-forgiving expression with any of those expression types. +reference to a property, or a null-forgiving expression with any of those expression types. For example, `5`, `"value"`, `DayOfWeek.Tuesday`, `int.MaxValue` and `default!` are all supported expressions for property initializers. @@ -230,7 +230,7 @@ partial class GeneratedManager ``` The source generator will find all command classes in your project, and generate C# code to provide -those command to the generated command manager without needing reflection. +those commands to the generated command manager without needing reflection. If you need to load commands from a different assembly, or multiple assemblies, you can use the [`GeneratedCommandManagerAttribute.AssemblyNames`][] property. This property can use either just the diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index cab8754e..84ab069e 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -8,7 +8,8 @@ does anything unsupported by Ookii.CommandLine. Among others, it checks for thin - Whether positional arguments have duplicate numbering. - Arguments with types that cannot be converted from a string. - Attribute or property combinations that are ignored. -- Using the [`CommandLineArgument`][] with a private member, or a method with an incorrect signature. +- Using the [`CommandLineArgumentAttribute`][] with a private member, or a method with an incorrect + signature. Without source generation, these mistakes would either lead to a runtime exception when creating the [`CommandLineParser`][] class, or would be silently ignored. With source generation, you can instead @@ -217,14 +218,12 @@ class, so a generated parser would not be used. For example, the following code triggers this error: -TODO: Update with span/memory if used. - ```csharp [Command] [GeneratedParser] partial class Arguments : ICommandWithCustomParsing // ERROR: The command uses custom parsing. { - public void Parse(string[] args, int index, CommandOptions options) + public void Parse(ReadOnlyMemory args, CommandManager manager) { // Omitted } @@ -249,11 +248,11 @@ For example, the following code triggers this error: [GeneratedParser] partial class Arguments { - [CommandLineAttribute(Position = 0)] + [CommandLineAttribute(IsPositional = true)] public string[]? Argument1 { get; set; } // ERROR: Argument2 comes after Argument1, which is multi-value. - [CommandLineAttribute(Position = 1)] + [CommandLineAttribute(IsPositional = true] public string? Argument2 { get; set; } } ``` @@ -270,12 +269,12 @@ For example, the following code triggers this error: [GeneratedParser] partial class Arguments { - [CommandLineAttribute(Position = 0)] + [CommandLineAttribute(IsPositional = true)] public string? Argument1 { get; set; } // ERROR: Required argument Argument2 comes after Argument1, which is optional. - [CommandLineAttribute(IsRequired = true, Position = 1)] - public string? Argument2 { get; set; } + [CommandLineAttribute(IsPositional = true)] + public required string Argument2 { get; set; } } ``` @@ -540,8 +539,8 @@ partial class Arguments The same position value is used for two or more arguments. -While the actual position values do not matter--merely the order of the values do, so skipping -numbers is fine--using the same number more than once can lead to unpredictable or unstable ordering +While the actual position values do not matter—merely the order of the values do, so skipping +numbers is fine—using the same number more than once can lead to unpredictable or unstable ordering of the arguments, which should be avoided. ```csharp @@ -560,7 +559,7 @@ partial class Arguments ### OCL0023 The [`ShortAliasAttribute`][] is ignored on an argument that does not have a short name. Set the -[`CommandLineArgumentAttribute.IsShort`][] property to true set an explicit short name using the +[`CommandLineArgumentAttribute.IsShort`][] property to true or set an explicit short name using the [`CommandLineArgumentAttribute.ShortName`][] property. Without a short name, any short aliases will not be used. @@ -863,8 +862,8 @@ Instead, the attribute should be applied to the assembly: The initial value of a property will not be included in the usage help, because it uses an expression type that is not supported by the source generator. Supported expression types are -literals, enumeration values, constants, and null-forgiving expressions containing any of those -expression types. +literals, enumeration values, constants, properties, and null-forgiving expressions containing any +of those expression types. For example, `5`, `"value"`, `DayOfWeek.Tuesday`, `int.MaxValue` and `default!` are all supported expressions for property initializers. @@ -875,7 +874,6 @@ supported and will not be included in the usage help. For example, the following code triggers this warning: ```csharp -// WARNING: ApplicationFriendlyName is ignored for commands. [GeneratedParser] partial class Arguments { @@ -910,7 +908,6 @@ Note that default values set by property initializers are only shown in the usag [`ArgumentConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ArgumentConverterAttribute.htm [`CommandAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandAttribute_IsHidden.htm [`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm -[`CommandLineArgument`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineArgument.htm [`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm [`CommandLineArgumentAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm [`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm diff --git a/docs/Subcommands.md b/docs/Subcommands.md index 54c52c9f..90597311 100644 --- a/docs/Subcommands.md +++ b/docs/Subcommands.md @@ -174,7 +174,7 @@ accomplished by having a common base class for each command that needs the commo ```csharp abstract class DatabaseCommand : ICommand { - [CommandLineArgument(Position = 0, IsRequired = true)] + [CommandLineArgument(IsPositional = true, IsRequired = true)] public string? ConnectionString { get; set; } public abstract int Run(); @@ -184,7 +184,7 @@ abstract class DatabaseCommand : ICommand [Command] partial class AddCommand : DatabaseCommand { - [CommandLineArgument(Position = 1, IsRequired = true)] + [CommandLineArgument(IsPositional = true, IsRequired = true)] public string? NewValue { get; set; } public override int Run() @@ -197,7 +197,7 @@ partial class AddCommand : DatabaseCommand [Command] partial class DeleteCommand : DatabaseCommand { - [CommandLineArgument(Position = 1, IsRequired = true)] + [CommandLineArgument(IsPositional = true, IsRequired = true)] public int Id { get; set; } [CommandLineArgument] @@ -211,7 +211,8 @@ partial class DeleteCommand : DatabaseCommand ``` The two commands, `AddCommand` and `DeleteCommand` both inherit the `-ConnectionString` argument, and -add their own additional arguments. +add their own additional arguments. When using the [`CommandLineArgumentAttribute.IsPositional`][] +property, base class arguments come before derived class arguments. The `DatabaseCommand` class is not considered a subcommand by the [`CommandManager`][], because it does not have the [`CommandAttribute`][] attribute, and because it is abstract. It also does not @@ -668,6 +669,7 @@ detail. [`CancelMode.Success`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CancelMode.htm [`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm [`CommandLineArgumentAttribute.CancelParsing`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_CancelParsing.htm +[`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm [`CommandLineParser.Parse()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_CommandLineParser_Parse__1.htm [`CommandLineParser`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm [`CommandManager.GetCommand()`]: https://www.ookii.org/docs/commandline-4.0/html/M_Ookii_CommandLine_Commands_CommandManager_GetCommand.htm diff --git a/docs/Utilities.md b/docs/Utilities.md index 3bbffcc3..31f069f1 100644 --- a/docs/Utilities.md +++ b/docs/Utilities.md @@ -63,7 +63,7 @@ writer.WriteLine(); writer.WriteLine("After a blank line, no indentation is used."); writer.WriteLine("The next line is indented again."); writer.ResetIndent(); -writer.WriteLine("This line is not."); +writer.WriteLine("This line is not because ResetIndent was called."); writer.WriteLine("And this one is."); ``` @@ -76,16 +76,16 @@ The first line is not indented. This line is pretty long, so it'll probably be w After a blank line, no indentation is used. The next line is indented again. -This line is not. +This line is not because ResetIndent was called. And this one is. ``` ## Virtual terminal support -Virtual terminal (VT) sequences are a method to manipulate the console utilized, supported by many -console applications on many operating systems. It is supported by the console host on recent +Virtual terminal (VT) sequences are a method to manipulate the console output, supported by many +terminal applications on many operating systems. It is supported by the console host on recent versions of Windows, by [Windows Terminal](https://learn.microsoft.com/windows/terminal/install), -and many console applications on other platforms. +and many terminal applications on other platforms. A VT sequence consists of an escape character, followed by a string that specifies what action to take. They can be used to set colors and other formatting options, but also to do things like move From ee2e7ed1dae56780972e098daed4c44c7a9efe46 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 14:46:13 -0700 Subject: [PATCH 210/234] Warn for ICommand without CommandAttribute. --- docs/SourceGenerationDiagnostics.md | 8 +++++--- src/Ookii.CommandLine.Generator/Diagnostics.cs | 8 ++++++++ .../ParserGenerator.cs | 9 ++++++--- .../ParserIncrementalGenerator.cs | 2 ++ .../Properties/Resources.Designer.cs | 18 ++++++++++++++++++ .../Properties/Resources.resx | 6 ++++++ 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 84ab069e..3e8426f0 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -471,7 +471,7 @@ partial class Arguments ### OCL0019 A command line arguments class has the [`CommandAttribute`][] but does not implement the [`ICommand`][] -interface. +interface, or vice versa. Without the interface, the [`CommandAttribute`][] is ignored and the class will not be treated as a command by a regular or generated [`CommandManager`][]. Both the [`CommandAttribute`][] and the [`ICommand`][] @@ -489,8 +489,10 @@ partial class MyCommand // WARNING: The class doesn't implement ICommand } ``` -The inverse, implementing [`ICommand`][] without using the [`CommandAttribute`][], does not generate a -warning as this is a common pattern for subcommand base classes. +The inverse, implementing [`ICommand`][] without using the [`CommandAttribute`][], can be used for +subcommand base classes. This still triggers a warning with the [`GeneratedParserAttribute`][], +since that attribute does not need to be applied to base classes, only to the derived classes that +are actually used as commands. ### OCL0020 diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index f20ffb45..e0e28575 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -183,6 +183,14 @@ public static Diagnostic CommandAttributeWithoutInterface(INamedTypeSymbol symbo symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic CommandInterfaceWithoutAttribute(INamedTypeSymbol symbol) => CreateDiagnostic( + "OCL0019", // Intentially the same as above. + nameof(Resources.CommandInterfaceWithoutAttributeTitle), + nameof(Resources.CommandInterfaceWithoutAttributeMessageFormat), + DiagnosticSeverity.Warning, + symbol.Locations.FirstOrDefault(), + symbol.ToDisplayString()); + public static Diagnostic DefaultValueWithRequired(ISymbol symbol) => CreateDiagnostic( "OCL0020", nameof(Resources.DefaultValueIgnoredTitle), diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index fe67dbc5..aa828978 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -88,12 +88,15 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen } else { - // The other way around (interface without attribute) doesn't need a warning since - // it could be a base class for a command (though it's kind of weird that the - // GeneratedParserAttribute was used on a base class). _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; diff --git a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs index 8897dc31..a057a139 100644 --- a/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserIncrementalGenerator.cs @@ -64,6 +64,8 @@ private static void Execute(Compilation compilation, ImmutableArray } 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)); } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index c2887a08..28fe6597 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -204,6 +204,24 @@ internal static string CommandAttributeWithoutInterfaceTitle { } } + /// + /// 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.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index c5bed80b..cfb199ad 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -165,6 +165,12 @@ The command line arguments class has the CommandAttribute but does not implement ICommand. + + 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. From 92b0d72a97be365cb2651742c0132a44701448c2 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 14:59:06 -0700 Subject: [PATCH 211/234] Warn for IsHidden on required arguments. --- docs/SourceGenerationDiagnostics.md | 12 ++++++++++-- src/Ookii.CommandLine.Generator/Diagnostics.cs | 6 +++--- src/Ookii.CommandLine.Generator/ParserGenerator.cs | 4 ++-- .../Properties/Resources.Designer.cs | 12 ++++++------ .../Properties/Resources.resx | 8 ++++---- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 3e8426f0..09b1fb47 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -558,6 +558,10 @@ partial class Arguments } ``` +When using the [`GeneratedParserAttribute`][], you can use the [`CommandLineArgumentAttribute.IsPositional`][] +property to create positional arguments by their definition order, without having to worry about +keeping explicitly set numbers correct. + ### OCL0023 The [`ShortAliasAttribute`][] is ignored on an argument that does not have a short name. Set the @@ -608,12 +612,16 @@ partial class Arguments ### OCL0025 -The [`CommandLineArgumentAttribute.IsHidden`][] property is ignored for positional arguments. +The [`CommandLineArgumentAttribute.IsHidden`][] property is ignored for positional or required +arguments. Positional arguments cannot be hidden, because excluding them from the usage help would give incorrect positions for any additional positional arguments. A positional argument is therefore not hidden even if [`IsHidden`][IsHidden_1] is set to true. +Required arguments cannot be hidden, because the application cannot be easily used if they user does +not know about them. + For example, the following code triggers this warning: ```csharp @@ -621,7 +629,7 @@ For example, the following code triggers this warning: partial class Arguments { // WARNING: The argument is not hidden because it's positional. - [CommandLineAttribute(Position = 0, IsHidden = true)] + [CommandLineAttribute(IsPositional = true, IsHidden = true)] public string? Argument { get; set; } } ``` diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index e0e28575..0606b479 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -248,10 +248,10 @@ public static Diagnostic AliasWithoutLongName(AttributeData attribute, ISymbol s attribute.GetLocation(), symbol.ToDisplayString()); - public static Diagnostic IsHiddenWithPositional(ISymbol symbol) => CreateDiagnostic( + public static Diagnostic IsHiddenWithPositionalOrRequired(ISymbol symbol) => CreateDiagnostic( "OCL0025", - nameof(Resources.IsHiddenWithPositionalTitle), - nameof(Resources.IsHiddenWithPositionalMessageFormat), + nameof(Resources.IsHiddenWithPositionalOrRequiredTitle), + nameof(Resources.IsHiddenWithPositionalOrRequiredMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index aa828978..fa9b11c6 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -617,9 +617,9 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> bool isHidden = false; if (argumentInfo.IsHidden) { - if (argumentInfo.Position != null) + if (argumentInfo.IsPositional || argumentInfo.IsRequired || (property?.IsRequired ?? false)) { - _context.ReportDiagnostic(Diagnostics.IsHiddenWithPositional(member)); + _context.ReportDiagnostic(Diagnostics.IsHiddenWithPositionalOrRequired(member)); } else { diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index 28fe6597..e6e890fc 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -475,20 +475,20 @@ internal static string InvalidMethodSignatureTitle { } /// - /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsHidden property is ignored for the argument defined by {0} because it is positional.. + /// 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 IsHiddenWithPositionalMessageFormat { + internal static string IsHiddenWithPositionalOrRequiredMessageFormat { get { - return ResourceManager.GetString("IsHiddenWithPositionalMessageFormat", resourceCulture); + return ResourceManager.GetString("IsHiddenWithPositionalOrRequiredMessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsHidden property is ignored for positional arguments.. + /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsHidden property is ignored for positional or required arguments.. /// - internal static string IsHiddenWithPositionalTitle { + internal static string IsHiddenWithPositionalOrRequiredTitle { get { - return ResourceManager.GetString("IsHiddenWithPositionalTitle", resourceCulture); + return ResourceManager.GetString("IsHiddenWithPositionalOrRequiredTitle", resourceCulture); } } diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index cfb199ad..080b4d94 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -255,11 +255,11 @@ 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. + + 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 arguments. + + The CommandLineArgumentAttribute.IsHidden property is ignored for positional or required arguments. The CommandLineArgumentAttribute.IsRequired property is ignored for the required property {0}. From 4b9ce1981856df519fece19afe0b0c1f06c79f04 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 15:09:05 -0700 Subject: [PATCH 212/234] Don't warn for unsupported initializer if default not shown in usage help. --- docs/SourceGenerationDiagnostics.md | 3 +++ .../CommandLineArgumentAttributeInfo.cs | 6 ++++++ src/Ookii.CommandLine.Generator/ParserGenerator.cs | 2 +- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 3 +++ src/Ookii.CommandLine.Tests/CommandLineParserTest.cs | 2 ++ 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 09b1fb47..c5c21098 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -906,6 +906,8 @@ the property's description manually, if desired. To avoid this warning, use one of the supported expression types, or use the [`CommandLineArgumentAttribute.DefaultValue`][] property. This warning will not be emitted if the [`CommandLineArgumentAttribute.DefaultValue`][] property is not null, regardless of the initializer. +It will also not be emitted if the [`CommandLineArgumentAttribute.IncludeDefaultInUsageHelp`][] +property is false. Note that default values set by property initializers are only shown in the usage help if the [`GeneratedParserAttribute`][] is used. When reflection is used, only @@ -919,6 +921,7 @@ Note that default values set by property initializers are only shown in the usag [`CommandAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_Commands_CommandAttribute_IsHidden.htm [`CommandAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandAttribute.htm [`CommandLineArgumentAttribute.DefaultValue`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_DefaultValue.htm +[`CommandLineArgumentAttribute.IncludeDefaultInUsageHelp`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IncludeDefaultInUsageHelp.htm [`CommandLineArgumentAttribute.IsHidden`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm [`CommandLineArgumentAttribute.IsLong`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsLong.htm [`CommandLineArgumentAttribute.IsPositional`]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsPositional.htm diff --git a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs index 0f375544..e9bf1086 100644 --- a/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs +++ b/src/Ookii.CommandLine.Generator/CommandLineArgumentAttributeInfo.cs @@ -56,6 +56,10 @@ public CommandLineArgumentAttributeInfo(AttributeData data) case nameof(IsHidden): IsHidden = (bool)named.Value.Value!; break; + + case nameof(IncludeDefaultInUsageHelp): + IncludeDefaultInUsageHelp = (bool)named.Value.Value!; + break; } } } @@ -81,4 +85,6 @@ public CommandLineArgumentAttributeInfo(AttributeData data) public bool IsLong { get; } = true; public bool IsHidden { get; } + + public bool IncludeDefaultInUsageHelp { get; set; } = true; } diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index fa9b11c6..e35b14af 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -506,7 +506,7 @@ private bool GenerateArgument(ISymbol member, ref List<(string, string, string)> } // Check if we should use the initializer for a default value. - if (!isMultiValue && !property.IsRequired && !argumentInfo.IsRequired && argumentInfo.DefaultValue == null) + if (!isMultiValue && !property.IsRequired && !argumentInfo.IsRequired && argumentInfo.DefaultValue == null && argumentInfo.IncludeDefaultInUsageHelp) { var alternateDefaultValue = GetInitializerValue(property); if (alternateDefaultValue != null) diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index c2011aeb..3c30f520 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -635,6 +635,9 @@ partial class InitializerDefaultValueArguments public string? Arg9 { get; set; } = null!; #nullable disable + [CommandLineArgument(IncludeDefaultInUsageHelp = false)] + public int Arg10 { get; set; } = 10; + private const int Value = 47; public static int GetValue() => 42; diff --git a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs index cc12b79c..2a18c765 100644 --- a/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs +++ b/src/Ookii.CommandLine.Tests/CommandLineParserTest.cs @@ -1248,6 +1248,8 @@ public void TestInitializerDefaultValues() 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] From d01655fc9e57089f3bd1863be87d9f0dd6f5bb13 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 15:12:03 -0700 Subject: [PATCH 213/234] Fixed inverted error numbers. --- src/Ookii.CommandLine.Generator/Diagnostics.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 0606b479..09ad0ba1 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -358,22 +358,22 @@ public static Diagnostic UnsupportedLanguageVersion(ISymbol symbol, string attri symbol.ToDisplayString(), attributeName); - public static Diagnostic UnsupportedInitializerSyntax(ISymbol symbol, Location location) => CreateDiagnostic( - "OCL0038", - nameof(Resources.UnsupportedInitializerSyntaxTitle), - nameof(Resources.UnsupportedInitializerSyntaxMessageFormat), - DiagnosticSeverity.Warning, - location, - symbol.ToDisplayString()); - public static Diagnostic MixedImplicitExplicitPositions(ISymbol symbol) => CreateDiagnostic( - "OCL0039", + "OCL0038", nameof(Resources.MixedImplicitExplicitPositionsTitle), nameof(Resources.MixedImplicitExplicitPositionsMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), symbol.ToDisplayString()); + public static Diagnostic UnsupportedInitializerSyntax(ISymbol symbol, Location location) => CreateDiagnostic( + "OCL0039", + nameof(Resources.UnsupportedInitializerSyntaxTitle), + nameof(Resources.UnsupportedInitializerSyntaxMessageFormat), + DiagnosticSeverity.Warning, + location, + symbol.ToDisplayString()); + private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( new DiagnosticDescriptor( From 679f30551a8b52ee094fcc935331de0c0ce8ce5f Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 15:13:04 -0700 Subject: [PATCH 214/234] Fix suppression. --- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 3c30f520..948d5066 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -14,7 +14,7 @@ #nullable disable // We deliberately have some properties and methods that cause warnings, so disable those. -#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029,OCL0033,OCL0038 +#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029,OCL0033,OCL0039 namespace Ookii.CommandLine.Tests; From f556de9892e7785b6221c3459f0d04638b8d3f8d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 15:32:48 -0700 Subject: [PATCH 215/234] Diagnostics proofreading and symbol updates. --- .../Diagnostics.cs | 93 +++++++++---------- .../Properties/Resources.Designer.cs | 28 +++--- .../Properties/Resources.resx | 28 +++--- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 2 +- 4 files changed, 72 insertions(+), 79 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index 09ad0ba1..ce3cd32b 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -13,7 +13,7 @@ public static Diagnostic TypeNotReferenceType(INamedTypeSymbol symbol, string at nameof(Resources.TypeNotReferenceTypeMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), attributeName); public static Diagnostic ClassNotPartial(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( @@ -22,7 +22,7 @@ public static Diagnostic ClassNotPartial(INamedTypeSymbol symbol, string attribu nameof(Resources.ClassNotPartialMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), attributeName); public static Diagnostic ClassIsGeneric(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( @@ -31,7 +31,7 @@ public static Diagnostic ClassIsGeneric(INamedTypeSymbol symbol, string attribut nameof(Resources.ClassIsGenericMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), attributeName); public static Diagnostic ClassIsNested(INamedTypeSymbol symbol, string attributeName) => CreateDiagnostic( @@ -40,7 +40,7 @@ public static Diagnostic ClassIsNested(INamedTypeSymbol symbol, string attribute nameof(Resources.ClassIsNestedMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), attributeName); @@ -50,8 +50,7 @@ public static Diagnostic InvalidArrayRank(IPropertySymbol property) => CreateDia nameof(Resources.InvalidArrayRankMessageFormat), DiagnosticSeverity.Error, property.Locations.FirstOrDefault(), - property.ContainingType?.ToDisplayString(), - property.Name); + property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => CreateDiagnostic( "OCL0006", @@ -59,8 +58,7 @@ public static Diagnostic PropertyIsReadOnly(IPropertySymbol property) => CreateD nameof(Resources.PropertyIsReadOnlyMessageFormat), DiagnosticSeverity.Error, property.Locations.FirstOrDefault(), - property.ContainingType?.ToDisplayString(), - property.Name); + property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => CreateDiagnostic( "OCL0007", @@ -68,9 +66,8 @@ public static Diagnostic NoConverter(ISymbol member, ITypeSymbol elementType) => nameof(Resources.NoConverterMessageFormat), DiagnosticSeverity.Error, member.Locations.FirstOrDefault(), - elementType.ToDisplayString(), - member.ContainingType?.ToDisplayString(), - member.Name); + elementType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), + member.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic InvalidMethodSignature(ISymbol method) => CreateDiagnostic( "OCL0008", @@ -78,8 +75,7 @@ public static Diagnostic InvalidMethodSignature(ISymbol method) => CreateDiagnos nameof(Resources.InvalidMethodSignatureMessageFormat), DiagnosticSeverity.Error, method.Locations.FirstOrDefault(), - method.ContainingType?.ToDisplayString(), - method.Name); + method.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic NonRequiredInitOnlyProperty(IPropertySymbol property) => CreateDiagnostic( "OCL0009", @@ -87,8 +83,7 @@ public static Diagnostic NonRequiredInitOnlyProperty(IPropertySymbol property) = nameof(Resources.NonRequiredInitOnlyPropertyMessageFormat), DiagnosticSeverity.Error, property.Locations.FirstOrDefault(), - property.ContainingType?.ToDisplayString(), - property.Name); + property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic GeneratedCustomParsingCommand(INamedTypeSymbol symbol) => CreateDiagnostic( "OCL0010", @@ -96,7 +91,7 @@ public static Diagnostic GeneratedCustomParsingCommand(INamedTypeSymbol symbol) nameof(Resources.GeneratedCustomParsingCommandMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic PositionalArgumentAfterMultiValue(ISymbol symbol, string other) => CreateDiagnostic( "OCL0011", @@ -104,7 +99,7 @@ public static Diagnostic PositionalArgumentAfterMultiValue(ISymbol symbol, strin nameof(Resources.PositionalArgumentAfterMultiValueMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), other); public static Diagnostic PositionalRequiredArgumentAfterOptional(ISymbol symbol, string other) => CreateDiagnostic( @@ -113,7 +108,7 @@ public static Diagnostic PositionalRequiredArgumentAfterOptional(ISymbol symbol, nameof(Resources.PositionalRequiredArgumentAfterOptionalMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), other); public static Diagnostic InvalidAssemblyName(ISymbol symbol, string name) => CreateDiagnostic( @@ -138,7 +133,7 @@ public static Diagnostic ArgumentConverterStringNotSupported(AttributeData attri nameof(Resources.ArgumentConverterStringNotSupportedMessageFormat), DiagnosticSeverity.Error, attribute.GetLocation(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic ParentCommandStringNotSupported(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( "OCL0015", // Intentially the same as above. @@ -146,7 +141,7 @@ public static Diagnostic ParentCommandStringNotSupported(AttributeData attribute nameof(Resources.ParentCommandStringNotSupportedMessageFormat), DiagnosticSeverity.Error, attribute.GetLocation(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic IgnoredAttribute(ISymbol symbol, AttributeData attribute) => CreateDiagnostic( "OCL0016", @@ -154,8 +149,8 @@ public static Diagnostic IgnoredAttribute(ISymbol symbol, AttributeData attribut nameof(Resources.UnknownAttributeMessageFormat), DiagnosticSeverity.Warning, attribute.GetLocation(), - attribute.AttributeClass?.ToDisplayString(), - symbol.ToDisplayString()); + attribute.AttributeClass?.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic NonPublicStaticMethod(ISymbol method) => CreateDiagnostic( "OCL0017", @@ -163,8 +158,7 @@ public static Diagnostic NonPublicStaticMethod(ISymbol method) => CreateDiagnost nameof(Resources.NonPublicStaticMethodMessageFormat), DiagnosticSeverity.Warning, method.Locations.FirstOrDefault(), - method.ContainingType?.ToDisplayString(), - method.Name); + method.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic NonPublicInstanceProperty(ISymbol property) => CreateDiagnostic( "OCL0018", @@ -172,8 +166,7 @@ public static Diagnostic NonPublicInstanceProperty(ISymbol property) => CreateDi nameof(Resources.NonPublicInstancePropertyMessageFormat), DiagnosticSeverity.Warning, property.Locations.FirstOrDefault(), - property.ContainingType?.ToDisplayString(), - property.Name); + property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic CommandAttributeWithoutInterface(INamedTypeSymbol symbol) => CreateDiagnostic( "OCL0019", @@ -181,7 +174,7 @@ public static Diagnostic CommandAttributeWithoutInterface(INamedTypeSymbol symbo nameof(Resources.CommandAttributeWithoutInterfaceMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic CommandInterfaceWithoutAttribute(INamedTypeSymbol symbol) => CreateDiagnostic( "OCL0019", // Intentially the same as above. @@ -189,7 +182,7 @@ public static Diagnostic CommandInterfaceWithoutAttribute(INamedTypeSymbol symbo nameof(Resources.CommandInterfaceWithoutAttributeMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic DefaultValueWithRequired(ISymbol symbol) => CreateDiagnostic( "OCL0020", @@ -197,7 +190,7 @@ public static Diagnostic DefaultValueWithRequired(ISymbol symbol) => CreateDiagn nameof(Resources.DefaultValueWithRequiredMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic DefaultValueWithMultiValue(ISymbol symbol) => CreateDiagnostic( "OCL0020", // Deliberately the same as above. @@ -205,7 +198,7 @@ public static Diagnostic DefaultValueWithMultiValue(ISymbol symbol) => CreateDia nameof(Resources.DefaultValueWithMultiValueMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic DefaultValueWithMethod(ISymbol symbol) => CreateDiagnostic( "OCL0020", // Deliberately the same as above. @@ -213,7 +206,7 @@ public static Diagnostic DefaultValueWithMethod(ISymbol symbol) => CreateDiagnos nameof(Resources.DefaultValueWithMethodMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic IsRequiredWithRequiredProperty(ISymbol symbol) => CreateDiagnostic( "OCL0021", @@ -221,7 +214,7 @@ public static Diagnostic IsRequiredWithRequiredProperty(ISymbol symbol) => Creat nameof(Resources.IsRequiredWithRequiredPropertyMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic DuplicatePosition(ISymbol symbol, string otherName) => CreateDiagnostic( "OCL0022", @@ -229,7 +222,7 @@ public static Diagnostic DuplicatePosition(ISymbol symbol, string otherName) => nameof(Resources.DuplicatePositionMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), otherName); public static Diagnostic ShortAliasWithoutShortName(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( @@ -238,7 +231,7 @@ public static Diagnostic ShortAliasWithoutShortName(AttributeData attribute, ISy nameof(Resources.ShortAliasWithoutShortNameMessageFormat), DiagnosticSeverity.Warning, attribute.GetLocation(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic AliasWithoutLongName(AttributeData attribute, ISymbol symbol) => CreateDiagnostic( "OCL0024", @@ -246,7 +239,7 @@ public static Diagnostic AliasWithoutLongName(AttributeData attribute, ISymbol s nameof(Resources.AliasWithoutLongNameMessageFormat), DiagnosticSeverity.Warning, attribute.GetLocation(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic IsHiddenWithPositionalOrRequired(ISymbol symbol) => CreateDiagnostic( "OCL0025", @@ -254,7 +247,7 @@ public static Diagnostic IsHiddenWithPositionalOrRequired(ISymbol symbol) => Cre nameof(Resources.IsHiddenWithPositionalOrRequiredMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic InvalidGeneratedConverterNamespace(string ns, AttributeData attribute) => CreateDiagnostic( "OCL0026", @@ -271,7 +264,7 @@ public static Diagnostic IgnoredAttributeForNonDictionary(ISymbol member, Attrib DiagnosticSeverity.Warning, attribute.GetLocation(), attribute.AttributeClass?.Name, - member.ToDisplayString()); + member.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic IgnoredAttributeForDictionaryWithConverter(ISymbol member, AttributeData attribute) => CreateDiagnostic( "OCL0028", @@ -280,7 +273,7 @@ public static Diagnostic IgnoredAttributeForDictionaryWithConverter(ISymbol memb DiagnosticSeverity.Warning, attribute.GetLocation(), attribute.AttributeClass?.Name, - member.ToDisplayString()); + member.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic IgnoredAttributeForNonMultiValue(ISymbol member, AttributeData attribute) => CreateDiagnostic( "OCL0029", @@ -289,7 +282,7 @@ public static Diagnostic IgnoredAttributeForNonMultiValue(ISymbol member, Attrib DiagnosticSeverity.Warning, attribute.GetLocation(), attribute.AttributeClass?.Name, - member.ToDisplayString()); + member.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic ArgumentStartsWithNumber(ISymbol member, string name) => CreateDiagnostic( "OCL0030", @@ -298,7 +291,7 @@ public static Diagnostic ArgumentStartsWithNumber(ISymbol member, string name) = DiagnosticSeverity.Warning, member.Locations.FirstOrDefault(), name, - member.ToDisplayString()); + member.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic NoLongOrShortName(ISymbol member, AttributeData attribute) => CreateDiagnostic( "OCL0031", @@ -306,7 +299,7 @@ public static Diagnostic NoLongOrShortName(ISymbol member, AttributeData attribu nameof(Resources.NoLongOrShortNameMessageFormat), DiagnosticSeverity.Error, attribute.GetLocation(), - member.ToDisplayString()); + member.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic IsShortIgnored(ISymbol member, AttributeData attribute) => CreateDiagnostic( "OCL0032", @@ -314,7 +307,7 @@ public static Diagnostic IsShortIgnored(ISymbol member, AttributeData attribute) nameof(Resources.IsShortIgnoredMessageFormat), DiagnosticSeverity.Warning, attribute.GetLocation(), - member.ToDisplayString()); + member.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic ArgumentWithoutDescription(ISymbol member) => CreateDiagnostic( "OCL0033", @@ -322,7 +315,7 @@ public static Diagnostic ArgumentWithoutDescription(ISymbol member) => CreateDia nameof(Resources.ArgumentWithoutDescriptionMessageFormat), DiagnosticSeverity.Warning, member.Locations.FirstOrDefault(), - member.ToDisplayString()); + member.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic CommandWithoutDescription(ISymbol symbol) => CreateDiagnostic( "OCL0034", @@ -330,7 +323,7 @@ public static Diagnostic CommandWithoutDescription(ISymbol symbol) => CreateDiag nameof(Resources.CommandWithoutDescriptionMessageFormat), DiagnosticSeverity.Warning, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic IgnoredAttributeForNonCommand(ISymbol symbol, AttributeData attribute) => CreateDiagnostic( "OCL0035", @@ -338,8 +331,8 @@ public static Diagnostic IgnoredAttributeForNonCommand(ISymbol symbol, Attribute nameof(Resources.IgnoredAttributeForNonCommandMessageFormat), DiagnosticSeverity.Warning, attribute.GetLocation(), - attribute.AttributeClass?.ToDisplayString(), - symbol.ToDisplayString()); + attribute.AttributeClass?.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic IgnoredFriendlyNameAttribute(ISymbol symbol, AttributeData attribute) => CreateDiagnostic( "OCL0036", @@ -347,7 +340,7 @@ public static Diagnostic IgnoredFriendlyNameAttribute(ISymbol symbol, AttributeD nameof(Resources.IgnoredFriendlyNameAttributeMessageFormat), DiagnosticSeverity.Warning, attribute.GetLocation(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic UnsupportedLanguageVersion(ISymbol symbol, string attributeName) => CreateDiagnostic( "OCL0037", @@ -355,7 +348,7 @@ public static Diagnostic UnsupportedLanguageVersion(ISymbol symbol, string attri nameof(Resources.UnsupportedLanguageVersionMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString(), + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), attributeName); public static Diagnostic MixedImplicitExplicitPositions(ISymbol symbol) => CreateDiagnostic( @@ -364,7 +357,7 @@ public static Diagnostic MixedImplicitExplicitPositions(ISymbol symbol) => Creat nameof(Resources.MixedImplicitExplicitPositionsMessageFormat), DiagnosticSeverity.Error, symbol.Locations.FirstOrDefault(), - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic UnsupportedInitializerSyntax(ISymbol symbol, Location location) => CreateDiagnostic( "OCL0039", @@ -372,7 +365,7 @@ public static Diagnostic UnsupportedInitializerSyntax(ISymbol symbol, Location l nameof(Resources.UnsupportedInitializerSyntaxMessageFormat), DiagnosticSeverity.Warning, location, - symbol.ToDisplayString()); + symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); private static Diagnostic CreateDiagnostic(string id, string titleResource, string messageResource, DiagnosticSeverity severity, Location? location, params object?[]? messageArgs) => Diagnostic.Create( diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index e6e890fc..a5cc6a83 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -79,7 +79,7 @@ internal static string AliasWithoutLongNameTitle { } /// - /// Looks up a localized string similar to The command line argument defined by {0} uses the ArgumentConverterAttribute with a string argument, which is not supported by the GeneratedParserAttribute. Use a Type argument instead by using the typeof keyword.. + /// 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 { @@ -196,7 +196,7 @@ internal static string CommandAttributeWithoutInterfaceMessageFormat { } /// - /// Looks up a localized string similar to The command line arguments class has the CommandAttribute but does not implement ICommand.. + /// 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 { @@ -340,7 +340,7 @@ internal static string IgnoredAttributeForNonCommandMessageFormat { } /// - /// Looks up a localized string similar to The attribute is not used for command line arguments classes that are not commands.. + /// 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 { @@ -376,7 +376,7 @@ internal static string IgnoredAttributeForNonMultiValueMessageFormat { } /// - /// Looks up a localized string similar to The attribute is not used for a non-dictionary argument.. + /// Looks up a localized string similar to The attribute is not used for a non-multi-value argument.. /// internal static string IgnoredAttributeForNonMultiValueTitle { get { @@ -403,7 +403,7 @@ internal static string IgnoredFriendlyNameAttributeTitle { } /// - /// Looks up a localized string similar to The multi-value command line argument defined by {0}.{1} must have an array rank of one.. + /// 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 { @@ -457,7 +457,7 @@ internal static string InvalidGeneratedConverterNamespaceTitle { } /// - /// Looks up a localized string similar to The method {0}.{1} does not have a valid signature for a command line argument.. + /// 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 { @@ -493,7 +493,7 @@ internal static string IsHiddenWithPositionalOrRequiredTitle { } /// - /// Looks up a localized string similar to The CommandLineArgumentAttribute.IsRequired property is ignored for the required property {0}.. + /// 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 { @@ -547,7 +547,7 @@ internal static string MixedImplicitExplicitPositionsTitle { } /// - /// Looks up a localized string similar to No command line argument converter exists for type {0} used by the argument defined by {1}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter.. + /// 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 { @@ -583,7 +583,7 @@ internal static string NoLongOrShortNameTitle { } /// - /// Looks up a localized string similar to The property {0}.{1} will not create a command line argument because it is not a public instance property.. + /// 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 { @@ -601,7 +601,7 @@ internal static string NonPublicInstancePropertyTitle { } /// - /// Looks up a localized string similar to The method {0}.{1} will not create a command line argument because it is not a public static method.. + /// 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 { @@ -619,7 +619,7 @@ internal static string NonPublicStaticMethodTitle { } /// - /// Looks up a localized string similar to The command line argument property {0}.{1} may only have an 'init' accessor if the property is also declared as 'required'.. + /// 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 { @@ -637,7 +637,7 @@ internal static string NonRequiredInitOnlyPropertyTitle { } /// - /// Looks up a localized string similar to The subcommand defined by {0} uses the ParentCommandAttribute with a string argument, which is not supported by the GeneratedParserAttribute. Use a Type argument instead by using the typeof keyword.. + /// 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 { @@ -691,7 +691,7 @@ internal static string PositionalRequiredArgumentAfterOptionalTitle { } /// - /// Looks up a localized string similar to The property {0}.{1} must have a public set accessor.. + /// Looks up a localized string similar to The property {0} must have a public set accessor.. /// internal static string PropertyIsReadOnlyMessageFormat { get { @@ -781,7 +781,7 @@ internal static string UnknownAttributeTitle { } /// - /// 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, or constant. Consider changing the initializer, or use CommandLineArgumentAttribute.DefaultValue as an alternative.. + /// 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 { diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index 080b4d94..abbdbad0 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -124,7 +124,7 @@ The AliasAttribute is ignored on an argument with no long name. - The command line argument defined by {0} uses the ArgumentConverterAttribute with a string argument, which is not supported by the GeneratedParserAttribute. Use a Type argument instead by using the typeof keyword. + 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. @@ -163,7 +163,7 @@ 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 ICommand. + 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. @@ -211,7 +211,7 @@ 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 commands. + 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}. @@ -223,7 +223,7 @@ The {0} attribute is ignored for the non-multi-value argument defined by {1}. - The attribute is not used for a non-dictionary argument. + 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. @@ -232,7 +232,7 @@ The ApplicationFriendlyNameAttribute is ignored on a subcommand. - The multi-value command line argument defined by {0}.{1} must have an array rank of one. + 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. @@ -250,7 +250,7 @@ The specified namespace for generated converters is not valid. - The method {0}.{1} does not have a valid signature for a command line argument. + The method {0} does not have a valid signature for a command line argument. A method command line argument has an invalid signature. @@ -262,7 +262,7 @@ The CommandLineArgumentAttribute.IsHidden property is ignored for positional or required arguments. - The CommandLineArgumentAttribute.IsRequired property is ignored for the required property {0}. + 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. @@ -280,7 +280,7 @@ 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}.{2}, and none could be generated. Use the Ookii.CommandLine.Conversion.ArgumentConverterAttribute to specify a custom converter. + 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. @@ -292,25 +292,25 @@ Argument has neither a long nor short name. - The property {0}.{1} will not create a command line argument because it is not a public instance property. + 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}.{1} will not create a command line argument because it is not a public static method. + 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}.{1} may only have an 'init' accessor if the property is also declared as 'required'. + 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 argument, which is not supported by the GeneratedParserAttribute. Use a Type argument instead by using the typeof keyword. + 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. @@ -328,7 +328,7 @@ Required positional arguments must come before optional positional arguments. - The property {0}.{1} must have a public set accessor. + The property {0} must have a public set accessor. A command line argument property must have a public set accessor. @@ -358,7 +358,7 @@ Unknown attribute will be ignored. - The initial value of the property '{0}' will not be included in the usage help because it is not a literal expression, enum value, or constant. Consider changing the initializer, or use CommandLineArgumentAttribute.DefaultValue as an alternative. + 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. diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 948d5066..351ea800 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -14,7 +14,7 @@ #nullable disable // We deliberately have some properties and methods that cause warnings, so disable those. -#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029,OCL0033,OCL0039 +#pragma warning disable OCL0017,OCL0018,OCL0020,OCL0023,OCL0029,OCL0033,OCL0038,OCL0039 namespace Ookii.CommandLine.Tests; From 963472405d8e2e8a32f6e7c5d028e25bba5e65a7 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 16:00:19 -0700 Subject: [PATCH 216/234] Proofread sample documentation. --- src/Samples/ArgumentDependencies/README.md | 8 ++++---- src/Samples/LongShort/README.md | 2 +- src/Samples/NestedCommands/README.md | 6 +++--- src/Samples/Subcommand/README.md | 7 +++---- src/Samples/TopLevelArguments/README.md | 2 +- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Samples/ArgumentDependencies/README.md b/src/Samples/ArgumentDependencies/README.md index 42525a8c..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 diff --git a/src/Samples/LongShort/README.md b/src/Samples/LongShort/README.md index f44d102a..6b337975 100644 --- a/src/Samples/LongShort/README.md +++ b/src/Samples/LongShort/README.md @@ -15,7 +15,7 @@ equivalent to the following: ValueDescriptionTransform = NameTransform.DashCase)] ``` -This sample uses the same arguments as the [parser Sample](../Parser), so see that sample's source +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 diff --git a/src/Samples/NestedCommands/README.md b/src/Samples/NestedCommands/README.md index f8ae29b4..877d138b 100644 --- a/src/Samples/NestedCommands/README.md +++ b/src/Samples/NestedCommands/README.md @@ -13,7 +13,7 @@ Child commands are just regular commands using the [`CommandLineParser`][], and 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 creates a simple "database" application that lets your add and remove students and +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). @@ -72,7 +72,7 @@ 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. -- Remove the `version` command. +- 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 the [`ParentCommand`][] uses [`ICommandWithCustomParsing`][], @@ -104,7 +104,7 @@ 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-4.0/html/T_Ookii_CommandLine_CommandLineParser.htm [`CommandManager`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Commands_CommandManager.htm diff --git a/src/Samples/Subcommand/README.md b/src/Samples/Subcommand/README.md index 55877da2..a690d3e8 100644 --- a/src/Samples/Subcommand/README.md +++ b/src/Samples/Subcommand/README.md @@ -11,10 +11,9 @@ For detailed information, check the source of the [`ReadCommand`](ReadCommand.cs 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. A publish profile for Visual Studio that trims the application -is included so you can try this out, or you can run `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. +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. diff --git a/src/Samples/TopLevelArguments/README.md b/src/Samples/TopLevelArguments/README.md index 986f6208..d50c1bf7 100644 --- a/src/Samples/TopLevelArguments/README.md +++ b/src/Samples/TopLevelArguments/README.md @@ -20,7 +20,7 @@ arguments last, and to indicate additional command-specific arguments can follow 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 help when writing the command list as part +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. From 27919438b8968d2fc708f5e1bd88b0950ae3b0bf Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 16:03:55 -0700 Subject: [PATCH 217/234] Fix link formatting. --- docs/Validation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Validation.md b/docs/Validation.md index 599c442a..52af1025 100644 --- a/docs/Validation.md +++ b/docs/Validation.md @@ -9,8 +9,8 @@ custom property setters that perform the validation, Ookii.CommandLine also prov attributes. The advantage of this is that you can reuse common validation rules, if you use one of the generated [`Parse()`][Parse()_7], static [`CommandLineParser.Parse()`][] or [`CommandLineParser.ParseWithErrorHandling()`][] methods it will handle printing validation error -messages, and validators can also add a help message to the argument descriptions in the [usage -help](UsageHelp.md). +messages, and validators can also add a help message to the argument descriptions in the +[usage help](UsageHelp.md). ## Built-in validators From 28013abc0244efa046fa359b97e1a49d8b86eaef Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 16:15:26 -0700 Subject: [PATCH 218/234] Add package readme. --- .../Ookii.CommandLine.csproj | 5 +- src/Ookii.CommandLine/PackageReadme.md | 60 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/Ookii.CommandLine/PackageReadme.md diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index c73f710a..50152ce1 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -15,6 +15,8 @@ true ookii.snk false + en-US + Ookii.CommandLine Ookii.CommandLine is a powerful command line parsing library for .Net applications. - Easily define arguments by creating a class with properties. @@ -28,7 +30,7 @@ icon.png true true - en-US + PackageReadme.md @@ -49,6 +51,7 @@ + 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). From 66b50f358c2463901e3aba8c4ae3c4052626ce18 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 16:17:27 -0700 Subject: [PATCH 219/234] NuGet package updates. --- .../Ookii.CommandLine.Tests.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj index 4d295e4d..a183328a 100644 --- a/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj +++ b/src/Ookii.CommandLine.Tests/Ookii.CommandLine.Tests.csproj @@ -14,10 +14,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 7e8bc09b3d27c56e18e36a139f3250cfa67c0811 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 16:26:31 -0700 Subject: [PATCH 220/234] Update package description. --- src/Ookii.CommandLine/Ookii.CommandLine.csproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index 50152ce1..2f152df8 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -17,12 +17,14 @@ false en-US Ookii.CommandLine - Ookii.CommandLine is a powerful command line 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. +- 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 true From 799d160589daf3f5e59d524c090cdb26c36f2a8c Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 16:29:11 -0700 Subject: [PATCH 221/234] Ensure consistent URL capitalization. --- src/Ookii.CommandLine/Commands/CommandManager.cs | 2 +- src/Ookii.CommandLine/Ookii.CommandLine.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ookii.CommandLine/Commands/CommandManager.cs b/src/Ookii.CommandLine/Commands/CommandManager.cs index b07b3b2b..f56d8d18 100644 --- a/src/Ookii.CommandLine/Commands/CommandManager.cs +++ b/src/Ookii.CommandLine/Commands/CommandManager.cs @@ -45,7 +45,7 @@ namespace Ookii.CommandLine.Commands; /// /// /// -/// Usage documentation +/// Usage documentation /// public class CommandManager { diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index 2f152df8..9e0d311d 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -8,8 +8,8 @@ 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 From 928b49a06f2669849aedf312b33c81479d58886d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 6 Jul 2023 16:30:57 -0700 Subject: [PATCH 222/234] Updated release notes. --- src/Ookii.CommandLine/Ookii.CommandLine.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ookii.CommandLine/Ookii.CommandLine.csproj b/src/Ookii.CommandLine/Ookii.CommandLine.csproj index 9e0d311d..88e97b23 100644 --- a/src/Ookii.CommandLine/Ookii.CommandLine.csproj +++ b/src/Ookii.CommandLine/Ookii.CommandLine.csproj @@ -26,7 +26,7 @@ library for .Net applications. - 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 From cc7461fa489478ecf887716dc4654174a04368c9 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 13 Jul 2023 16:43:37 -0700 Subject: [PATCH 223/234] Remove global prefix from generated converter names. --- src/Directory.Build.props | 2 +- src/Ookii.CommandLine.Generator/ConverterGenerator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 06c3b98e..3dac151d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,6 +5,6 @@ Ookii.org Copyright (c) Sven Groot (Ookii.org) 4.0.0 - preview + preview2 \ No newline at end of file diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 33dbd673..8e3c56cb 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -65,7 +65,7 @@ public ConverterGenerator(TypeHelper typeHelper, SourceProductionContext context return null; } - info.Name = GenerateName(type.ToQualifiedName()); + info.Name = GenerateName(type.ToDisplayString()); _converters.Add(type, info); converter = info; } From b71150d9a08094deac6e36532ea03abb84e6f485 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 13 Jul 2023 17:01:11 -0700 Subject: [PATCH 224/234] Add XML comments for generated public methods. --- .../ParserGenerator.cs | 47 +++++++++++++++++++ src/Ookii.CommandLine/IParser.cs | 6 +-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index e35b14af..01b8b452 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -154,6 +154,17 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen } } + _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); @@ -162,10 +173,46 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen { // 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 } diff --git a/src/Ookii.CommandLine/IParser.cs b/src/Ookii.CommandLine/IParser.cs index 9b4bdc2a..f62e6e4c 100644 --- a/src/Ookii.CommandLine/IParser.cs +++ b/src/Ookii.CommandLine/IParser.cs @@ -39,8 +39,7 @@ public interface IParser : IParserProvider /// /// /// An instance of the type , or if an - /// error occurred, or argument parsing was canceled by the - /// property or a method argument that returned . + /// error occurred or argument parsing was canceled. /// /// /// The cannot use type as the @@ -68,8 +67,7 @@ public interface IParser : IParserProvider /// /// /// An instance of the type , or if an - /// error occurred, or argument parsing was canceled by the - /// property or a method argument that returned . + /// error occurred or argument parsing was canceled. /// /// /// The cannot use type as the From 5038b7200fa8e83e618dc1c94c47af17815351f5 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 13 Jul 2023 17:42:01 -0700 Subject: [PATCH 225/234] Only warn for unused TypeConverterAttribute. --- docs/SourceGenerationDiagnostics.md | 36 ++++++++++++++----- .../ArgumentAttributes.cs | 18 +++++----- .../ArgumentsClassAttributes.cs | 16 ++------- .../CommandGenerator.cs | 4 +-- .../Diagnostics.cs | 7 ++-- .../ParserGenerator.cs | 2 +- .../Properties/Resources.Designer.cs | 36 +++++++++---------- .../Properties/Resources.resx | 12 +++---- src/Ookii.CommandLine.Generator/TypeHelper.cs | 2 ++ 9 files changed, 73 insertions(+), 60 deletions(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index c5c21098..d074c1ea 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -408,14 +408,13 @@ work without the [`GeneratedParserAttribute`][]. ### OCL0016 -Unknown attribute will be ignored. +The [`TypeConverterAttribute`][] is no longer used by Ookii.CommandLine, and will be ignored. -The arguments class itself, or one of the members defining an argument, has an attribute that is -not used by Ookii.CommandLine. +As of Ookii.CommandLine 4.0, argument values are converted from a string using the +[`ArgumentConverter`][] class and [`TypeConverter`][] is no longer used. Custom converters should be +specified using the [`ArgumentConverterAttribute`][] attribute. -For example, the following code triggers this warning, because the current version of -Ookii.CommandLine no longer uses the [`TypeConverterAttribute`][], having replaced it with the -[`ArgumentConverterAttribute`][]: +For example, the following code triggers this warning: ```csharp [GeneratedParser] @@ -427,8 +426,27 @@ partial class Arguments } ``` -To fix this warning, remove the relevant attribute. If the attribute is present for some purpose -other than Ookii.CommandLine, you should suppress or disable this warning. +To fix this warning, switch to using the [`ArgumentConverterAttribute`][] attribute. To use the +existing [`TypeConverter`][], you can inherit from the [`TypeConverterArgumentConverter`][] class. + +```csharp +public class MyArgumentConverter : TypeConverterArgumentConverter +{ + public MyArgumentConverter() + : base(new MyNamespace.MyConverter()) + { + } +} + +[GeneratedParser] +partial class Arguments +{ + [CommandLineAttribute] + [ArgumentConverter(typeof(MyArgumentConverter)] + public CustomType? Argument { get; set; } +} +``` + ### OCL0017 @@ -955,6 +973,8 @@ Note that default values set by property initializers are only shown in the usag [`ParsingMode.LongShort`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ParsingMode.htm [`ShortAliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ShortAliasAttribute.htm [`Type`]: https://learn.microsoft.com/dotnet/api/system.type +[`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter +[`TypeConverterArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter.htm [`TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute [`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm [IsHidden_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm diff --git a/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs index 483ed165..d9e5096f 100644 --- a/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs +++ b/src/Ookii.CommandLine.Generator/ArgumentAttributes.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using System; namespace Ookii.CommandLine.Generator; @@ -19,9 +20,10 @@ internal class ArgumentAttributes public ArgumentAttributes(ISymbol member, TypeHelper typeHelper, SourceProductionContext context) { + AttributeData? typeConverterAttribute = null; foreach (var attribute in member.GetAttributes()) { - if (attribute.CheckType(typeHelper.CommandLineArgumentAttribute, ref _commandLineArgumentAttribute) || + var _ = attribute.CheckType(typeHelper.CommandLineArgumentAttribute, ref _commandLineArgumentAttribute) || attribute.CheckType(typeHelper.MultiValueSeparatorAttribute, ref _multiValueSeparator) || attribute.CheckType(typeHelper.DescriptionAttribute, ref _description) || attribute.CheckType(typeHelper.ValueDescriptionAttribute, ref _valueDescription) || @@ -32,14 +34,14 @@ public ArgumentAttributes(ISymbol member, TypeHelper typeHelper, SourceProductio attribute.CheckType(typeHelper.ValueConverterAttribute, ref _valueConverterAttribute) || attribute.CheckType(typeHelper.AliasAttribute, ref _aliases) || attribute.CheckType(typeHelper.ShortAliasAttribute, ref _shortAliases) || - attribute.CheckType(typeHelper.ArgumentValidationAttribute, ref _validators) || - // Don't warn about attributes used by the compiler. - (attribute.AttributeClass?.ContainingNamespace.ToDisplayString().StartsWith("System.Runtime.CompilerServices") ?? false)) - { - continue; - } + attribute.CheckType(typeHelper.ArgumentValidationAttribute, ref _validators); + attribute.CheckType(typeHelper.TypeConverterAttribute, ref typeConverterAttribute); + } - context.ReportDiagnostic(Diagnostics.IgnoredAttribute(member, attribute)); + // Only warn if the TypeConverterAttribute is present. + if (CommandLineArgument != null && typeConverterAttribute != null) + { + context.ReportDiagnostic(Diagnostics.IgnoredTypeConverterAttribute(member, typeConverterAttribute)); } } diff --git a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs index 1748e079..73519641 100644 --- a/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs +++ b/src/Ookii.CommandLine.Generator/ArgumentsClassAttributes.cs @@ -13,31 +13,21 @@ internal readonly struct ArgumentsClassAttributes private readonly List? _classValidators; private readonly List? _aliases; - public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper, SourceProductionContext? context) + public ArgumentsClassAttributes(ITypeSymbol symbol, TypeHelper typeHelper) { // Exclude special types so we don't generate warnings for attributes on framework types. for (var current = symbol; current?.SpecialType == SpecialType.None; current = current.BaseType) { foreach (var attribute in current.GetAttributes()) { - if (attribute.CheckType(typeHelper.ParseOptionsAttribute, ref _parseOptions) || + var _ = attribute.CheckType(typeHelper.ParseOptionsAttribute, ref _parseOptions) || attribute.CheckType(typeHelper.DescriptionAttribute, ref _description) || attribute.CheckType(typeHelper.ApplicationFriendlyNameAttribute, ref _applicationFriendlyName) || attribute.CheckType(typeHelper.CommandAttribute, ref _command) || attribute.CheckType(typeHelper.ClassValidationAttribute, ref _classValidators) || attribute.CheckType(typeHelper.ParentCommandAttribute, ref _parentCommand) || attribute.CheckType(typeHelper.AliasAttribute, ref _aliases) || - attribute.CheckType(typeHelper.GeneratedParserAttribute, ref _generatedParser) || - // Don't warn about attributes used by the compiler. - (attribute.AttributeClass?.ContainingNamespace.ToDisplayString().StartsWith("System.Runtime.CompilerServices") ?? false)) - { - continue; - } - - if (context is SourceProductionContext c) - { - c.ReportDiagnostic(Diagnostics.IgnoredAttribute(current, attribute)); - } + attribute.CheckType(typeHelper.GeneratedParserAttribute, ref _generatedParser); } } } diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index fea1ac34..90c4c801 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -36,7 +36,7 @@ public override void VisitNamedType(INamedTypeSymbol symbol) { if (symbol.DeclaredAccessibility == Accessibility.Public && symbol.ImplementsInterface(_typeHelper.ICommand)) { - var attributes = new ArgumentsClassAttributes(symbol, _typeHelper, null); + var attributes = new ArgumentsClassAttributes(symbol, _typeHelper); if (attributes.Command != null) { Commands.Add((symbol, attributes)); @@ -184,7 +184,7 @@ private bool GenerateCommand(SourceBuilder builder, INamedTypeSymbol commandType builder.AppendArgument($"typeof({commandTypeName})"); } - var attributes = commandAttributes ?? new ArgumentsClassAttributes(commandType, _typeHelper, null); + var attributes = commandAttributes ?? new ArgumentsClassAttributes(commandType, _typeHelper); builder.AppendArgument($"{attributes.Command!.CreateInstantiation()}"); if (attributes.Description != null) { diff --git a/src/Ookii.CommandLine.Generator/Diagnostics.cs b/src/Ookii.CommandLine.Generator/Diagnostics.cs index ce3cd32b..dae08fc0 100644 --- a/src/Ookii.CommandLine.Generator/Diagnostics.cs +++ b/src/Ookii.CommandLine.Generator/Diagnostics.cs @@ -143,13 +143,12 @@ public static Diagnostic ParentCommandStringNotSupported(AttributeData attribute attribute.GetLocation(), symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); - public static Diagnostic IgnoredAttribute(ISymbol symbol, AttributeData attribute) => CreateDiagnostic( + public static Diagnostic IgnoredTypeConverterAttribute(ISymbol symbol, AttributeData attribute) => CreateDiagnostic( "OCL0016", - nameof(Resources.UnknownAttributeTitle), - nameof(Resources.UnknownAttributeMessageFormat), + nameof(Resources.IgnoredTypeConverterAttributeTitle), + nameof(Resources.IgnoredTypeConverterAttributeMessageFormat), DiagnosticSeverity.Warning, attribute.GetLocation(), - attribute.AttributeClass?.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); public static Diagnostic NonPublicStaticMethod(ISymbol method) => CreateDiagnostic( diff --git a/src/Ookii.CommandLine.Generator/ParserGenerator.cs b/src/Ookii.CommandLine.Generator/ParserGenerator.cs index 01b8b452..3611f4d9 100644 --- a/src/Ookii.CommandLine.Generator/ParserGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ParserGenerator.cs @@ -71,7 +71,7 @@ public ParserGenerator(SourceProductionContext context, INamedTypeSymbol argumen // 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, _context); + var attributes = new ArgumentsClassAttributes(_argumentsClass, _typeHelper); var isCommand = false; if (attributes.Command != null) diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs index a5cc6a83..7b474f6a 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.Designer.cs @@ -402,6 +402,24 @@ internal static string IgnoredFriendlyNameAttributeTitle { } } + /// + /// 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.. /// @@ -762,24 +780,6 @@ internal static string UnknownAssemblyNameTitle { } } - /// - /// Looks up a localized string similar to The attribute '{0}' on '{1}' is unknown and will be ignored by the GeneratedParserAttribute.. - /// - internal static string UnknownAttributeMessageFormat { - get { - return ResourceManager.GetString("UnknownAttributeMessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unknown attribute will be ignored.. - /// - internal static string UnknownAttributeTitle { - get { - return ResourceManager.GetString("UnknownAttributeTitle", 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.. /// diff --git a/src/Ookii.CommandLine.Generator/Properties/Resources.resx b/src/Ookii.CommandLine.Generator/Properties/Resources.resx index abbdbad0..40bd3799 100644 --- a/src/Ookii.CommandLine.Generator/Properties/Resources.resx +++ b/src/Ookii.CommandLine.Generator/Properties/Resources.resx @@ -231,6 +231,12 @@ 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. @@ -351,12 +357,6 @@ Unknown assembly name. - - The attribute '{0}' on '{1}' is unknown and will be ignored by the GeneratedParserAttribute. - - - Unknown attribute will be ignored. - 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. diff --git a/src/Ookii.CommandLine.Generator/TypeHelper.cs b/src/Ookii.CommandLine.Generator/TypeHelper.cs index 51c7a6fa..197e00a2 100644 --- a/src/Ookii.CommandLine.Generator/TypeHelper.cs +++ b/src/Ookii.CommandLine.Generator/TypeHelper.cs @@ -31,6 +31,8 @@ public TypeHelper(Compilation compilation) 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"); From c2d58482430fa41f1036fe06cfcec38ac11f0c08 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 13 Jul 2023 17:52:54 -0700 Subject: [PATCH 226/234] Improved TypeConverter wrapping. --- docs/DefiningArguments.md | 11 ++++---- docs/Migrating.md | 6 +++-- docs/SourceGenerationDiagnostics.md | 14 +++------- docs/refs.json | 2 -- src/Ookii.CommandLine.Tests/ArgumentTypes.cs | 2 +- ...eric.cs => WrappedDefaultTypeConverter.cs} | 11 +++++--- ...ntConverter.cs => WrappedTypeConverter.cs} | 15 ++++++----- .../Conversion/WrappedTypeConverterGeneric.cs | 26 +++++++++++++++++++ 8 files changed, 55 insertions(+), 32 deletions(-) rename src/Ookii.CommandLine/Conversion/{TypeConverterArgumentConverterGeneric.cs => WrappedDefaultTypeConverter.cs} (52%) rename src/Ookii.CommandLine/Conversion/{TypeConverterArgumentConverter.cs => WrappedTypeConverter.cs} (70%) create mode 100644 src/Ookii.CommandLine/Conversion/WrappedTypeConverterGeneric.cs diff --git a/docs/DefiningArguments.md b/docs/DefiningArguments.md index 425de30e..5658ddbf 100644 --- a/docs/DefiningArguments.md +++ b/docs/DefiningArguments.md @@ -343,11 +343,11 @@ Ookii.CommandLine 4.0, this is no longer the case, and the [`ArgumentConverter`] instead. To help with transitioning code that relied on [`TypeConverter`][], you can use the -[`TypeConverterArgumentConverter`][] class to use a type's default argument converter. +[`WrappedDefaultTypeConverter`][] class to use a type's default type converter. ```csharp [CommandLineArgument] -[ArgumentConverter(typeof(TypeConverterArgumentConverter))] +[ArgumentConverter(typeof(WrappedDefaultTypeConverter))] public SomeType Argument { get; set; } ``` @@ -355,8 +355,7 @@ This will use [`TypeDescriptor.GetConverter()`][] function to get the default [` 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. -If you were using a custom [`TypeConverter`][], you can use the [`TypeConverterArgumentConverter`][] class -as a base class to adapt it. +If you were using a custom [`TypeConverter`][], you can use the [`WrappedTypeConverter`][] class. ### Arguments that cancel parsing @@ -664,10 +663,10 @@ Next, we'll take a look at how to [parse the arguments we've defined](ParsingArg [`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 -[`TypeConverterArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter.htm -[`TypeConverterArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter_1.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 diff --git a/docs/Migrating.md b/docs/Migrating.md index 37752194..86a32897 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -25,7 +25,8 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ converters must be specified using the [`ArgumentConverterAttribute`][] instead of the [`TypeConverterAttribute`][]. - If you have existing conversions that depend on a [`TypeConverter`][], use the - [`TypeConverterArgumentConverter`][] as a convenient way to keep using that conversion. + [`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`][] @@ -189,12 +190,13 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`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 -[`TypeConverterArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter_1.htm [`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 diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index d074c1ea..1d748cb7 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -427,22 +427,14 @@ partial class Arguments ``` To fix this warning, switch to using the [`ArgumentConverterAttribute`][] attribute. To use the -existing [`TypeConverter`][], you can inherit from the [`TypeConverterArgumentConverter`][] class. +existing [`TypeConverter`][], you can use the [`WrappedTypeConverter`][] class. ```csharp -public class MyArgumentConverter : TypeConverterArgumentConverter -{ - public MyArgumentConverter() - : base(new MyNamespace.MyConverter()) - { - } -} - [GeneratedParser] partial class Arguments { [CommandLineAttribute] - [ArgumentConverter(typeof(MyArgumentConverter)] + [ArgumentConverter(typeof(WrappedTypeConverter)] public CustomType? Argument { get; set; } } ``` @@ -974,7 +966,7 @@ Note that default values set by property initializers are only shown in the usag [`ShortAliasAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_ShortAliasAttribute.htm [`Type`]: https://learn.microsoft.com/dotnet/api/system.type [`TypeConverter`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverter -[`TypeConverterArgumentConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter.htm +[`WrappedTypeConverter`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_WrappedTypeConverter_1.htm [`TypeConverterAttribute`]: https://learn.microsoft.com/dotnet/api/system.componentmodel.typeconverterattribute [`ValueConverterAttribute`]: https://www.ookii.org/docs/commandline-4.0/html/T_Ookii_CommandLine_Conversion_ValueConverterAttribute.htm [IsHidden_1]: https://www.ookii.org/docs/commandline-4.0/html/P_Ookii_CommandLine_CommandLineArgumentAttribute_IsHidden.htm diff --git a/docs/refs.json b/docs/refs.json index f9c916f7..46095986 100644 --- a/docs/refs.json +++ b/docs/refs.json @@ -347,8 +347,6 @@ "ToString()": "#system.object.tostring", "Type": "#system.type", "TypeConverter": "#system.componentmodel.typeconverter", - "TypeConverterArgumentConverter": "T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter", - "TypeConverterArgumentConverter": "T_Ookii_CommandLine_Conversion_TypeConverterArgumentConverter_1", "TypeConverterAttribute": "#system.componentmodel.typeconverterattribute", "TypeDescriptor.GetConverter()": "#system.componentmodel.typedescriptor.getconverter", "Uri": "#system.uri", diff --git a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs index 351ea800..c946f370 100644 --- a/src/Ookii.CommandLine.Tests/ArgumentTypes.cs +++ b/src/Ookii.CommandLine.Tests/ArgumentTypes.cs @@ -50,7 +50,7 @@ partial class TestArguments [CommandLineArgument("other2", DefaultValue = "47", Position = 5), Description("Arg4 description.")] [ValueDescription("Number")] [ValidateRange(0, 1000, IncludeInUsageHelp = false)] - [ArgumentConverter(typeof(TypeConverterArgumentConverter))] + [ArgumentConverter(typeof(WrappedDefaultTypeConverter))] public int Arg4 { get; set; } // Short/long name stuff should be ignored if not using LongShort mode. diff --git a/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverterGeneric.cs b/src/Ookii.CommandLine/Conversion/WrappedDefaultTypeConverter.cs similarity index 52% rename from src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverterGeneric.cs rename to src/Ookii.CommandLine/Conversion/WrappedDefaultTypeConverter.cs index 79edb94a..ce7a6ff2 100644 --- a/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverterGeneric.cs +++ b/src/Ookii.CommandLine/Conversion/WrappedDefaultTypeConverter.cs @@ -8,16 +8,21 @@ namespace Ookii.CommandLine.Conversion; /// 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 TypeConverterArgumentConverter : TypeConverterArgumentConverter +public class WrappedDefaultTypeConverter : WrappedTypeConverter { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public TypeConverterArgumentConverter() + public WrappedDefaultTypeConverter() : base(TypeDescriptor.GetConverter(typeof(T))) { } diff --git a/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs b/src/Ookii.CommandLine/Conversion/WrappedTypeConverter.cs similarity index 70% rename from src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs rename to src/Ookii.CommandLine/Conversion/WrappedTypeConverter.cs index 8c0e3917..10920000 100644 --- a/src/Ookii.CommandLine/Conversion/TypeConverterArgumentConverter.cs +++ b/src/Ookii.CommandLine/Conversion/WrappedTypeConverter.cs @@ -5,20 +5,21 @@ namespace Ookii.CommandLine.Conversion; /// -/// An that wraps an existing for a -/// type. +/// An that wraps an existing . /// /// /// -/// For a convenient way to use the default for a type, use the -/// class. +/// 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 TypeConverterArgumentConverter : ArgumentConverter +public class WrappedTypeConverter : ArgumentConverter { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The to use. /// @@ -28,7 +29,7 @@ public class TypeConverterArgumentConverter : ArgumentConverter /// The specified by cannot convert /// from a . /// - public TypeConverterArgumentConverter(TypeConverter converter) + public WrappedTypeConverter(TypeConverter converter) { Converter = converter ?? throw new ArgumentNullException(nameof(converter)); if (!converter.CanConvertFrom(typeof(string))) 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()) + { + } +} From 29006dc0c775c3136e248284b108207c4b08031d Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Thu, 13 Jul 2023 17:58:47 -0700 Subject: [PATCH 227/234] Remove extra blank line. --- docs/SourceGenerationDiagnostics.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/SourceGenerationDiagnostics.md b/docs/SourceGenerationDiagnostics.md index 1d748cb7..b88eaf3d 100644 --- a/docs/SourceGenerationDiagnostics.md +++ b/docs/SourceGenerationDiagnostics.md @@ -439,7 +439,6 @@ partial class Arguments } ``` - ### OCL0017 Methods that are not public and static will be ignored. From 2c3918ba7da8680088822321f8228a38ce3ad53c Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 14 Jul 2023 11:04:55 -0700 Subject: [PATCH 228/234] Don't use C# keywords in the name of generated converters. --- src/Ookii.CommandLine.Generator/CommandGenerator.cs | 2 +- src/Ookii.CommandLine.Generator/ConverterGenerator.cs | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Ookii.CommandLine.Generator/CommandGenerator.cs b/src/Ookii.CommandLine.Generator/CommandGenerator.cs index 90c4c801..b454c5b5 100644 --- a/src/Ookii.CommandLine.Generator/CommandGenerator.cs +++ b/src/Ookii.CommandLine.Generator/CommandGenerator.cs @@ -80,7 +80,7 @@ public void Generate() var source = GenerateManager(manager); if (source != null) { - _context.AddSource(manager.ToQualifiedName().ToIdentifier(".g.cs"), SourceText.From(source, Encoding.UTF8)); + _context.AddSource(manager.ToDisplayString().ToIdentifier(".g.cs"), SourceText.From(source, Encoding.UTF8)); } } } diff --git a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs index 8e3c56cb..5852e956 100644 --- a/src/Ookii.CommandLine.Generator/ConverterGenerator.cs +++ b/src/Ookii.CommandLine.Generator/ConverterGenerator.cs @@ -65,7 +65,7 @@ public ConverterGenerator(TypeHelper typeHelper, SourceProductionContext context return null; } - info.Name = GenerateName(type.ToDisplayString()); + info.Name = GenerateName(type); _converters.Add(type, info); converter = info; } @@ -170,8 +170,15 @@ public ConverterGenerator(TypeHelper typeHelper, SourceProductionContext context return info; } - private static string GenerateName(string displayName) + private static string GenerateName(ITypeSymbol type) { + // Use the full framework name even for types that have keywords, and don't include global + // namespace. + var format = SymbolDisplayFormat.FullyQualifiedFormat + .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted) + .RemoveMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + var displayName = type.ToDisplayString(format); return displayName.ToIdentifier(ConverterSuffix); } From 166ccedc3af53a7c3cf05d6fc3c82e919b67d755 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Fri, 14 Jul 2023 11:07:40 -0700 Subject: [PATCH 229/234] Reword source generation recommendation. --- docs/Migrating.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/Migrating.md b/docs/Migrating.md index 86a32897..d075041b 100644 --- a/docs/Migrating.md +++ b/docs/Migrating.md @@ -15,8 +15,8 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ ## Breaking API changes from version 3.0 -- It's strongly recommended to use [source generation](SourceGeneration.md) unless you cannot meet - the requirements. +- 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. @@ -166,6 +166,7 @@ As of version 3.0, .Net Framework 2.0 is no longer supported. You can still targ [`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 +[`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 From db66580ec9d5dfae4a4701bad3635d819f878e37 Mon Sep 17 00:00:00 2001 From: Sven Groot Date: Tue, 18 Jul 2023 13:20:49 -0700 Subject: [PATCH 230/234] Update SHFB project target framework. --- docs/Ookii.CommandLine.shfbproj | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/Ookii.CommandLine.shfbproj b/docs/Ookii.CommandLine.shfbproj index 87ad0203..e4b42177 100644 --- a/docs/Ookii.CommandLine.shfbproj +++ b/docs/Ookii.CommandLine.shfbproj @@ -27,21 +27,22 @@ <para> Provides functionality for defining and parsing command line arguments, and for generating usage help. </para> -<para> + <para> Provides functionality for creating applications with multiple subcommands, each with their own arguments. </para> -<para> + <para> Provides helpers for using virtual terminal sequences and color output on the console. </para> -<para> + <para> Provides attributes used to validate the value of arguments, and the relation between arguments. </para> -<para> + <para> Provides functionality for converting argument strings from the command line to the actual type of an argument. </para> -<para> + <para> Provides types to support source generation. Types in this namespace should not be used directly in your code. -</para> +</para> + https://github.com/SvenGroot/Ookii.CommandLine Copyright &#169%3b Sven Groot %28Ookii.org%29 Ookii.CommandLine 4.0 documentation @@ -89,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 +