12 Commits
2.1 ... 2.2

Author SHA1 Message Date
Tyrrrz
5e684c8b36 Update version 2022-01-11 00:40:30 +02:00
Tyrrrz
300ae70564 Update NuGet packages 2022-01-11 00:39:19 +02:00
Tyrrrz
76f0c77f1e Update readme 2022-01-11 00:32:56 +02:00
Tyrrrz
0f7cea4ed1 Add some more analyzer tests 2022-01-10 23:56:54 +02:00
Tyrrrz
32ee0b2bd6 Add test for optional parameters 2022-01-10 23:48:38 +02:00
Tyrrrz
4ff1e1d3e1 Cleanup 2022-01-10 23:41:28 +02:00
AliReZa Sabouri
8e96d2701d Add support for optional parameters (#119) 2022-01-10 13:11:04 -08:00
Tyrrrz
8e307df231 More cleanup 2022-01-10 16:55:43 +02:00
Tyrrrz
ff38f4916a Cleanup 2022-01-10 16:45:41 +02:00
AliReZa Sabouri
7cbbb220b4 Fix tests for default interface members (#121) 2022-01-09 20:29:57 -08:00
AliReZa Sabouri
ae2d4299f0 Add multiple inheritance support through interfaces (#120) 2022-01-09 08:11:42 -08:00
Tyrrrz
21bc69d116 Make projects not packable by default 2022-01-04 22:48:33 +02:00
43 changed files with 652 additions and 197 deletions

View File

@@ -1,3 +1,8 @@
### v2.2 (11-Jan-2022)
- Added support for optional parameters. A parameter can be marked as optional by setting `IsRequired = false` on the attribute. Only one parameter is allowed to be optional and such parameter must be the last in order. (Thanks [@AliReZa Sabouri](https://github.com/alirezanet))
- Fixed an issue where parameters and options bound to properties implemented as default interface members were not working correctly. (Thanks [@AliReZa Sabouri](https://github.com/alirezanet))
### v2.1 (04-Jan-2022)
- Added `IConsole.Clear()` with corresponding implementations in `SystemConsole`, `FakeConsole`, and `FakeInMemoryConsole`. (Thanks [@Alex Rosenfeld](https://github.com/alexrosenfeld10))

View File

@@ -2,8 +2,6 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat>
</PropertyGroup>
@@ -15,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Basic.Reference.Assemblies" Version="1.2.4" />
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="FluentAssertions" Version="6.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
<PackageReference Include="xunit" Version="2.4.1" />

View File

@@ -0,0 +1,94 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests;
public class ParameterMustBeLastIfNonRequiredAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeLastIfNonRequiredAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_non_required_parameter_is_not_the_last_in_order()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Name = ""foo"", IsRequired = false)]
public string Foo { get; set; }
[CommandParameter(1, Name = ""bar"")]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_non_required_parameter_is_the_last_in_order()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Name = ""foo"")]
public string Foo { get; set; }
[CommandParameter(1, Name = ""bar"", IsRequired = false)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Name = ""foo"")]
public string Foo { get; set; }
[CommandParameter(1, Name = ""bar"", IsRequired = true)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}

View File

@@ -9,7 +9,7 @@ public class ParameterMustBeLastIfNonScalarAnalyzerSpecs
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeLastIfNonScalarAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_a_non_scalar_parameter_is_not_last_in_order()
public void Analyzer_reports_an_error_if_a_non_scalar_parameter_is_not_the_last_in_order()
{
// Arrange
// language=cs
@@ -31,7 +31,7 @@ public class MyCommand : ICommand
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_is_last_in_order()
public void Analyzer_does_not_report_an_error_if_a_non_scalar_parameter_is_the_last_in_order()
{
// Arrange
// language=cs

View File

@@ -0,0 +1,94 @@
using CliFx.Analyzers.Tests.Utils;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests;
public class ParameterMustBeSingleIfNonRequiredAnalyzerSpecs
{
private static DiagnosticAnalyzer Analyzer { get; } = new ParameterMustBeSingleIfNonRequiredAnalyzer();
[Fact]
public void Analyzer_reports_an_error_if_more_than_one_non_required_parameters_are_defined()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, IsRequired = false)]
public string Foo { get; set; }
[CommandParameter(1, IsRequired = false)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().ProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_only_one_non_required_parameter_is_defined()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1, IsRequired = false)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_no_non_required_parameters_are_defined()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1, IsRequired = true)]
public string Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_on_a_property_that_is_not_a_parameter()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public string Foo { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -71,10 +71,9 @@ internal partial class CommandOptionSymbol
{
var attribute = TryGetOptionAttribute(property);
if (attribute is null)
return null;
return FromAttribute(attribute);
return attribute is not null
? FromAttribute(attribute)
: null;
}
public static bool IsOptionProperty(IPropertySymbol property) =>

View File

@@ -11,6 +11,8 @@ internal partial class CommandParameterSymbol
public string? Name { get; }
public bool? IsRequired { get; }
public ITypeSymbol? ConverterType { get; }
public IReadOnlyList<ITypeSymbol> ValidatorTypes { get; }
@@ -18,11 +20,13 @@ internal partial class CommandParameterSymbol
public CommandParameterSymbol(
int order,
string? name,
bool? isRequired,
ITypeSymbol? converterType,
IReadOnlyList<ITypeSymbol> validatorTypes)
{
Order = order;
Name = name;
IsRequired = isRequired;
ConverterType = converterType;
ValidatorTypes = validatorTypes;
}
@@ -48,6 +52,12 @@ internal partial class CommandParameterSymbol
.Select(a => a.Value.Value)
.FirstOrDefault() as string;
var isRequired = attribute
.NamedArguments
.Where(a => a.Key == "IsRequired")
.Select(a => a.Value.Value)
.FirstOrDefault() as bool?;
var converter = attribute
.NamedArguments
.Where(a => a.Key == "Converter")
@@ -63,17 +73,16 @@ internal partial class CommandParameterSymbol
.Cast<ITypeSymbol>()
.ToArray();
return new CommandParameterSymbol(order, name, converter, validators);
return new CommandParameterSymbol(order, name, isRequired, converter, validators);
}
public static CommandParameterSymbol? TryResolve(IPropertySymbol property)
{
var attribute = TryGetParameterAttribute(property);
if (attribute is null)
return null;
return FromAttribute(attribute);
return attribute is not null
? FromAttribute(attribute)
: null;
}
public static bool IsParameterProperty(IPropertySymbol property) =>

View File

@@ -0,0 +1,60 @@
using System.Linq;
using CliFx.Analyzers.ObjectModel;
using CliFx.Analyzers.Utils.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeLastIfNonRequiredAnalyzer : AnalyzerBase
{
public ParameterMustBeLastIfNonRequiredAnalyzer()
: base(
"Parameters marked as non-required must be the last in order",
"This parameter is non-required so it must be the last in order (its order must be highest within the command).")
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;
if (parameter.IsRequired != false)
return;
var otherProperties = property
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(m => !m.Equals(property, SymbolEqualityComparer.Default))
.ToArray();
foreach (var otherProperty in otherProperties)
{
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
if (otherParameter is null)
continue;
if (otherParameter.Order > parameter.Order)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

@@ -12,8 +12,8 @@ public class ParameterMustBeLastIfNonScalarAnalyzer : AnalyzerBase
{
public ParameterMustBeLastIfNonScalarAnalyzer()
: base(
"Parameters of non-scalar types must be last in order",
"This parameter has a non-scalar type so it must be last in order (its order must be highest within the command).")
"Parameters of non-scalar types must be the last in order",
"This parameter has a non-scalar type so it must be the last in order (its order must be highest within the command).")
{
}

View File

@@ -0,0 +1,60 @@
using System.Linq;
using CliFx.Analyzers.ObjectModel;
using CliFx.Analyzers.Utils.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ParameterMustBeSingleIfNonRequiredAnalyzer : AnalyzerBase
{
public ParameterMustBeSingleIfNonRequiredAnalyzer()
: base(
"Parameters marked as non-required are limited to one per command",
"This parameter is non-required so it must be the only such parameter in the command.")
{
}
private void Analyze(
SyntaxNodeAnalysisContext context,
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;
if (parameter.IsRequired != false)
return;
var otherProperties = property
.ContainingType
.GetMembers()
.OfType<IPropertySymbol>()
.Where(m => !m.Equals(property, SymbolEqualityComparer.Default))
.ToArray();
foreach (var otherProperty in otherProperties)
{
var otherParameter = CommandParameterSymbol.TryResolve(otherProperty);
if (otherParameter is null)
continue;
if (otherParameter.IsRequired == false)
{
context.ReportDiagnostic(CreateDiagnostic(propertyDeclaration.GetLocation()));
}
}
}
public override void Initialize(AnalysisContext context)
{
base.Initialize(context);
context.HandlePropertyDeclaration(Analyze);
}
}

View File

@@ -10,7 +10,7 @@
<PackageReference Include="clipr" Version="1.6.1" />
<PackageReference Include="Cocona" Version="1.6.0" />
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.1.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.0" />
<PackageReference Include="PowerArgs" Version="3.6.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" />
</ItemGroup>

View File

@@ -2,8 +2,6 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat>
</PropertyGroup>
@@ -14,8 +12,8 @@
<ItemGroup>
<PackageReference Include="Basic.Reference.Assemblies" Version="1.2.4" />
<PackageReference Include="CliWrap" Version="3.3.3" />
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="CliWrap" Version="3.4.0" />
<PackageReference Include="FluentAssertions" Version="6.3.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />

View File

@@ -496,6 +496,83 @@ public class Command : ICommand
);
}
[Fact]
public async Task Option_binding_supports_multiple_inheritance_through_default_interface_members()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
public static class SharedContext
{
public static int Foo { get; set; }
public static bool Bar { get; set; }
}
public interface IHasFoo : ICommand
{
[CommandOption(""foo"")]
public int Foo
{
get => SharedContext.Foo;
set => SharedContext.Foo = value;
}
}
public interface IHasBar : ICommand
{
[CommandOption(""bar"")]
public bool Bar
{
get => SharedContext.Bar;
set => SharedContext.Bar = value;
}
}
public interface IHasBaz : ICommand
{
public string Baz { get; set; }
}
[Command]
public class Command : IHasFoo, IHasBar, IHasBaz
{
[CommandOption(""baz"")]
public string Baz { get; set; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + SharedContext.Foo);
console.Output.WriteLine(""Bar = "" + SharedContext.Bar);
console.Output.WriteLine(""Baz = "" + Baz);
return default;
}
}
");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo", "42", "--bar", "--baz", "xyz" }
);
var stdOut = FakeConsole.ReadOutputString();
// Assert
exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = 42",
"Bar = True",
"Baz = xyz"
);
}
[Fact]
public async Task Option_binding_does_not_consider_a_negative_number_as_an_option_name_or_short_name()
{

View File

@@ -120,7 +120,53 @@ public class Command : ICommand
}
[Fact]
public async Task Parameter_binding_fails_if_one_of_the_parameters_has_not_been_provided()
public async Task Parameter_is_not_bound_if_there_are_no_arguments_matching_its_order()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
[Command]
public class Command : ICommand
{
[CommandParameter(0)]
public string Foo { get; set; }
[CommandParameter(1, IsRequired = false)]
public string Bar { get; set; } = ""xyz"";
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Foo = "" + Foo);
console.Output.WriteLine(""Bar = "" + Bar);
return default;
}
}");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"abc"},
new Dictionary<string, string>()
);
var stdOut = FakeConsole.ReadOutputString();
// Assert
exitCode.Should().Be(0);
stdOut.Should().ConsistOfLines(
"Foo = abc",
"Bar = xyz"
);
}
[Fact]
public async Task Parameter_binding_fails_if_a_required_parameter_has_not_been_provided()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
@@ -153,7 +199,7 @@ public class Command : ICommand
// Assert
exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing parameter(s)");
stdErr.Should().Contain("Missing required parameter(s)");
}
[Fact]
@@ -190,7 +236,7 @@ public class Command : ICommand
// Assert
exitCode.Should().NotBe(0);
stdErr.Should().Contain("Missing parameter(s)");
stdErr.Should().Contain("Missing required parameter(s)");
}
[Fact]

View File

@@ -29,7 +29,7 @@ public sealed class CommandOptionAttribute : Attribute
public char? ShortName { get; }
/// <summary>
/// Whether this option is required.
/// Whether this option is required (default: <c>false</c>).
/// If an option is required, the user will get an error if they don't set it.
/// </summary>
public bool IsRequired { get; set; }

View File

@@ -11,18 +11,26 @@ public sealed class CommandParameterAttribute : Attribute
{
/// <summary>
/// Parameter order.
/// </summary>
/// <remarks>
/// Higher order means the parameter appears later, lower order means
/// it appears earlier.
///
/// </summary>
/// <remarks>
/// All parameters in a command must have unique order.
///
/// Parameter whose type is a non-scalar (e.g. array), must always be the last in order.
/// Only one non-scalar parameter is allowed in a command.
/// </remarks>
public int Order { get; }
/// <summary>
/// Whether this parameter is required (default: <c>true</c>).
/// If a parameter is required, the user will get an error if they don't set it.
/// </summary>
/// <remarks>
/// Parameter marked as non-required must always be the last in order.
/// Only one non-required parameter is allowed in a command.
/// </remarks>
public bool IsRequired { get; set; } = true;
/// <summary>
/// Parameter name.
/// This is shown to the user in the help text.

View File

@@ -4,6 +4,7 @@
<TargetFrameworks>netstandard2.1;netstandard2.0</TargetFrameworks>
<Authors>$(Company)</Authors>
<Description>Declarative framework for building command line applications</Description>
<IsPackable>true</IsPackable>
<PackageTags>command line executable interface framework parser arguments cli app application net core</PackageTags>
<PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl>
<PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes>

View File

@@ -43,12 +43,6 @@ internal class CommandBinder
return string.IsNullOrWhiteSpace(rawValue) || bool.Parse(rawValue);
}
// IConvertible primitives (int, double, char, etc)
if (targetType.IsConvertible())
{
return Convert.ChangeType(rawValue, targetType, _formatProvider);
}
// Special case for DateTimeOffset
if (targetType == typeof(DateTimeOffset))
{
@@ -68,6 +62,12 @@ internal class CommandBinder
return Enum.Parse(targetType, rawValue!, true);
}
// Convertible primitives (int, double, char, etc)
if (targetType.Implements(typeof(IConvertible)))
{
return Convert.ChangeType(rawValue, targetType, _formatProvider);
}
// Nullable<T>
var nullableUnderlyingType = targetType.TryGetNullableUnderlyingType();
if (nullableUnderlyingType is not null)
@@ -223,7 +223,7 @@ internal class CommandBinder
{
// Ensure there are no unexpected parameters and that all parameters are provided
var remainingParameterInputs = commandInput.Parameters.ToList();
var remainingParameterSchemas = commandSchema.Parameters.ToList();
var remainingRequiredParameterSchemas = commandSchema.Parameters.Where(p => p.IsRequired).ToList();
var position = 0;
@@ -258,7 +258,7 @@ internal class CommandBinder
remainingParameterInputs.RemoveRange(parameterInputs);
}
remainingParameterSchemas.Remove(parameterSchema);
remainingRequiredParameterSchemas.Remove(parameterSchema);
}
if (remainingParameterInputs.Any())
@@ -272,12 +272,12 @@ internal class CommandBinder
);
}
if (remainingParameterSchemas.Any())
if (remainingRequiredParameterSchemas.Any())
{
throw CliFxException.UserError(
"Missing parameter(s):" +
"Missing required parameter(s):" +
Environment.NewLine +
remainingParameterSchemas
remainingRequiredParameterSchemas
.Select(o => o.GetFormattedIdentifier())
.JoinToString(" ")
);

View File

@@ -156,8 +156,16 @@ internal class HelpConsoleFormatter : ConsoleFormatter
WriteHeader("Parameters");
foreach (var parameterSchema in _context.CommandSchema.Parameters.OrderBy(p => p.Order))
{
if (parameterSchema.IsRequired)
{
Write(ConsoleColor.Red, "* ");
}
else
{
WriteHorizontalMargin();
}
Write(ConsoleColor.DarkCyan, $"{parameterSchema.Name}");
WriteColumnMargin();

View File

@@ -87,12 +87,27 @@ internal partial class CommandSchema
? new[] {OptionSchema.HelpOption, OptionSchema.VersionOption}
: new[] {OptionSchema.HelpOption};
var parameterSchemas = type.GetProperties()
var properties = type
// Get properties directly on command type
.GetProperties()
// Get non-abstract properties on interfaces (to support default interfaces members)
.Union(type
.GetInterfaces()
// Only interfaces implementing ICommand for explicitness
.Where(i => typeof(ICommand).IsAssignableFrom(i) && i != typeof(ICommand))
.SelectMany(i => i
.GetProperties()
.Where(p => !p.GetMethod.IsAbstract && !p.SetMethod.IsAbstract)
)
)
.ToArray();
var parameterSchemas = properties
.Select(ParameterSchema.TryResolve)
.WhereNotNull()
.ToArray();
var optionSchemas = type.GetProperties()
var optionSchemas = properties
.Select(OptionSchema.TryResolve)
.WhereNotNull()
.Concat(implicitOptionSchemas)

View File

@@ -13,6 +13,8 @@ internal partial class ParameterSchema : IMemberSchema
public string Name { get; }
public bool IsRequired { get; }
public string? Description { get; }
public Type? ConverterType { get; }
@@ -23,6 +25,7 @@ internal partial class ParameterSchema : IMemberSchema
IPropertyDescriptor property,
int order,
string name,
bool isRequired,
string? description,
Type? converterType,
IReadOnlyList<Type> validatorTypes)
@@ -30,6 +33,7 @@ internal partial class ParameterSchema : IMemberSchema
Property = property;
Order = order;
Name = name;
IsRequired = isRequired;
Description = description;
ConverterType = converterType;
ValidatorTypes = validatorTypes;
@@ -55,6 +59,7 @@ internal partial class ParameterSchema
new BindablePropertyDescriptor(property),
attribute.Order,
name,
attribute.IsRequired,
description,
attribute.Converter,
attribute.Validators

View File

@@ -59,27 +59,4 @@ internal static class TypeExtensions
var toStringMethod = type.GetMethod(nameof(ToString), Type.EmptyTypes);
return toStringMethod?.GetBaseDefinition()?.DeclaringType != toStringMethod?.DeclaringType;
}
// Types supported by `Convert.ChangeType(...)`
private static readonly HashSet<Type> ConvertibleTypes = new()
{
typeof(bool),
typeof(char),
typeof(sbyte),
typeof(byte),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong),
typeof(float),
typeof(double),
typeof(decimal),
typeof(DateTime),
typeof(string),
typeof(object)
};
public static bool IsConvertible(this Type type) => ConvertibleTypes.Contains(type);
}

View File

@@ -32,7 +32,6 @@ internal static partial class PolyfillExtensions
stream.Write(buffer, 0, buffer.Length);
}
namespace System.Linq
{
internal static class PolyfillExtensions

View File

@@ -1,12 +1,13 @@
<Project>
<PropertyGroup>
<Version>2.1</Version>
<Version>2.2</Version>
<Company>Tyrrrz</Company>
<Copyright>Copyright (C) Alexey Golub</Copyright>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>
<!-- Disable nullability warnings on older frameworks because there is no nullability info for BCL -->

View File

@@ -168,7 +168,7 @@ In case the user forgets to specify the `value` parameter, the application will
```sh
> dotnet myapp.dll -b 10
Missing parameter(s):
Missing required parameter(s):
<value>
```
@@ -193,11 +193,12 @@ OPTIONS
Overall, parameters and options are both used to consume input from the command line, but they differ in a few important ways:
- Parameters are identified by their relative order. Options are identified by their name or a single-character short name.
- Parameters technically also have a name, but it's only used in the help text.
- Parameters are always required. Options are normally optional, but can also be configured to require a value.
- Options can be configured to use an environment variable as a fallback.
- Both parameters and options can take multiple values, but there can only be one such parameter in a command and it must be the last in order. Options are not limited in this regard.
| | Parameters | Options |
|--------------------|-------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| **Identification** | Positional (by relative order). | Named (by name or short name). |
| **Requiredness** | Required by default. Only the last parameter can be configured to be optional. | Optional by default. Any option can be configured to be required without limitations. |
| **Arity** | Depends on the property type. Only the last parameter can be bound to a non-scalar type (i.e. array). | Depends on the property type. Any option can be bound to a non-scalar type without limitations. |
| **Fallback** | — | Can be configured to use an environment variable as fallback, in case the option isn't set. |
As a general guideline, it's recommended to use parameters for required inputs that the command can't function without.
Use options for all other non-required inputs or when specifying the name explicitly makes the usage clearer.
@@ -226,7 +227,7 @@ Similarly, unseparated arguments in the form of `myapp -ofile` will be treated a
Because of these rules, order of arguments is semantically important and must always follow this pattern:
```ini
```txt
[directives] [command name] [parameters] [options]
```