diff --git a/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj b/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj index 9433f93a0c..acab3a977e 100644 --- a/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj +++ b/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj @@ -2,7 +2,7 @@ true - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net7.0 This package provides support for using System.CommandLine with Microsoft.Extensions.Hosting. diff --git a/src/System.CommandLine.NamingConventionBinder/CommandHandler.cs b/src/System.CommandLine.NamingConventionBinder/CommandHandler.cs index a0fa5a5d5e..dbcf4daed9 100644 --- a/src/System.CommandLine.NamingConventionBinder/CommandHandler.cs +++ b/src/System.CommandLine.NamingConventionBinder/CommandHandler.cs @@ -605,7 +605,7 @@ public static ICommandHandler Create> action) => HandlerDescriptor.FromDelegate(action).GetCommandHandler(); - internal static async Task GetExitCodeAsync(object returnValue, InvocationContext context) + internal static async Task GetExitCodeAsync(object? returnValue, InvocationContext context) { switch (returnValue) { diff --git a/src/System.CommandLine.NamingConventionBinder/MethodInfoHandlerDescriptor.cs b/src/System.CommandLine.NamingConventionBinder/MethodInfoHandlerDescriptor.cs index c94b951149..6f4e09e744 100644 --- a/src/System.CommandLine.NamingConventionBinder/MethodInfoHandlerDescriptor.cs +++ b/src/System.CommandLine.NamingConventionBinder/MethodInfoHandlerDescriptor.cs @@ -38,7 +38,7 @@ public override ICommandHandler GetCommandHandler() } } - public override ModelDescriptor Parent => ModelDescriptor.FromType(_handlerMethodInfo.ReflectedType); + public override ModelDescriptor Parent => ModelDescriptor.FromType(_handlerMethodInfo.ReflectedType!); private protected override IEnumerable InitializeParameterDescriptors() => _handlerMethodInfo.GetParameters() diff --git a/src/System.CommandLine.NamingConventionBinder/ModelBinder.cs b/src/System.CommandLine.NamingConventionBinder/ModelBinder.cs index 08c52d510b..9eb6facc07 100644 --- a/src/System.CommandLine.NamingConventionBinder/ModelBinder.cs +++ b/src/System.CommandLine.NamingConventionBinder/ModelBinder.cs @@ -141,7 +141,7 @@ private bool ShortCutTheBinding() private (bool success, object? newInstance, bool anyNonDefaults) InstanceFromSpecificConstructor( BindingContext bindingContext, ConstructorDescriptor constructor, IReadOnlyList? boundValues, ref bool nonDefaultsUsed) { - var values = boundValues.Select(x => x.Value).ToArray(); + var values = boundValues?.Select(x => x.Value).ToArray() ?? Array.Empty(); object? newInstance = null; try { @@ -332,7 +332,7 @@ private static BoundValue DefaultForValueDescriptor( valueSource); } - private protected IValueDescriptor FindModelPropertyDescriptor(Type propertyType, string propertyName) + private protected IValueDescriptor? FindModelPropertyDescriptor(Type propertyType, string propertyName) { return ModelDescriptor.PropertyDescriptors .FirstOrDefault(desc => diff --git a/src/System.CommandLine.NamingConventionBinder/ModelBinder{T}.cs b/src/System.CommandLine.NamingConventionBinder/ModelBinder{T}.cs index 58acb092ae..98b7d95eba 100644 --- a/src/System.CommandLine.NamingConventionBinder/ModelBinder{T}.cs +++ b/src/System.CommandLine.NamingConventionBinder/ModelBinder{T}.cs @@ -25,10 +25,12 @@ public void BindMemberFromValue( IValueDescriptor valueDescriptor) { var (propertyType, propertyName) = property.MemberTypeAndName(); - var propertyDescriptor = FindModelPropertyDescriptor( - propertyType, propertyName); - MemberBindingSources[propertyDescriptor] = - new SpecificSymbolValueSource(valueDescriptor); + var propertyDescriptor = FindModelPropertyDescriptor(propertyType, propertyName); + + if (propertyDescriptor is not null) + { + MemberBindingSources[propertyDescriptor] = new SpecificSymbolValueSource(valueDescriptor); + } } /// @@ -42,9 +44,10 @@ public void BindMemberFromValue( Func getValue) { var (propertyType, propertyName) = property.MemberTypeAndName(); - var propertyDescriptor = FindModelPropertyDescriptor( - propertyType, propertyName); - MemberBindingSources[propertyDescriptor] = - new DelegateValueSource(c => getValue(c)); + var propertyDescriptor = FindModelPropertyDescriptor(propertyType, propertyName); + if (propertyDescriptor is not null) + { + MemberBindingSources[propertyDescriptor] = new DelegateValueSource(c => getValue(c)); + } } } \ No newline at end of file diff --git a/src/System.CommandLine.NamingConventionBinder/ModelBindingCommandHandler.cs b/src/System.CommandLine.NamingConventionBinder/ModelBindingCommandHandler.cs index 72a53338f6..451cb1591a 100644 --- a/src/System.CommandLine.NamingConventionBinder/ModelBindingCommandHandler.cs +++ b/src/System.CommandLine.NamingConventionBinder/ModelBindingCommandHandler.cs @@ -31,7 +31,7 @@ internal ModelBindingCommandHandler( _handlerMethodInfo = handlerMethodInfo ?? throw new ArgumentNullException(nameof(handlerMethodInfo)); _invocationTargetBinder = _handlerMethodInfo.IsStatic ? null - : new ModelBinder(_handlerMethodInfo.ReflectedType); + : new ModelBinder(_handlerMethodInfo.ReflectedType!); _methodDescriptor = methodDescriptor ?? throw new ArgumentNullException(nameof(methodDescriptor)); _invocationTarget = invocationTarget; } @@ -69,11 +69,11 @@ public async Task InvokeAsync(InvocationContext context) .Select(x => x.Value) .ToArray(); - object result; + object? result; if (_handlerDelegate is null) { var invocationTarget = _invocationTarget ?? - bindingContext.GetService(_handlerMethodInfo!.ReflectedType); + bindingContext.GetService(_handlerMethodInfo!.ReflectedType!); if(invocationTarget is { }) { _invocationTargetBinder?.UpdateInstance(invocationTarget, bindingContext); diff --git a/src/System.CommandLine.NamingConventionBinder/ModelDescriptor.cs b/src/System.CommandLine.NamingConventionBinder/ModelDescriptor.cs index d47cee393f..341919493c 100644 --- a/src/System.CommandLine.NamingConventionBinder/ModelDescriptor.cs +++ b/src/System.CommandLine.NamingConventionBinder/ModelDescriptor.cs @@ -46,7 +46,7 @@ protected ModelDescriptor(Type modelType) public IReadOnlyList PropertyDescriptors => _propertyDescriptors ??= ModelType.GetProperties(CommonBindingFlags) - .Where(p => p.CanWrite && p.SetMethod.IsPublic) + .Where(p => p.CanWrite && p.SetMethod?.IsPublic == true) .Select(i => new PropertyDescriptor(i, this)) .ToList(); diff --git a/src/System.CommandLine.NamingConventionBinder/ParameterDescriptor.cs b/src/System.CommandLine.NamingConventionBinder/ParameterDescriptor.cs index c7ae5fcb65..4cc264117b 100644 --- a/src/System.CommandLine.NamingConventionBinder/ParameterDescriptor.cs +++ b/src/System.CommandLine.NamingConventionBinder/ParameterDescriptor.cs @@ -23,7 +23,7 @@ internal ParameterDescriptor( } /// - public string ValueName => _parameterInfo.Name; + public string ValueName => _parameterInfo.Name!; /// /// The method descriptor that this constructor belongs to. diff --git a/src/System.CommandLine.NamingConventionBinder/System.CommandLine.NamingConventionBinder.csproj b/src/System.CommandLine.NamingConventionBinder/System.CommandLine.NamingConventionBinder.csproj index 567e87f251..3e9813d5f8 100644 --- a/src/System.CommandLine.NamingConventionBinder/System.CommandLine.NamingConventionBinder.csproj +++ b/src/System.CommandLine.NamingConventionBinder/System.CommandLine.NamingConventionBinder.csproj @@ -3,7 +3,7 @@ true System.CommandLine.NamingConventionBinder - netstandard2.0 + net7.0;netstandard2.0 10 enable This package provides command handler support for System.CommandLine performs parameter and model binding by matching option and argument names to parameter and property names. diff --git a/src/System.CommandLine.Tests/VersionOptionTests.cs b/src/System.CommandLine.Tests/VersionOptionTests.cs index 5d3a5ce7b7..fcebf7c1f9 100644 --- a/src/System.CommandLine.Tests/VersionOptionTests.cs +++ b/src/System.CommandLine.Tests/VersionOptionTests.cs @@ -73,7 +73,7 @@ public async Task When_the_version_option_is_specified_and_there_are_default_opt { var rootCommand = new RootCommand { - new Option("-x") + new Option("-x", defaultValueFactory: () => true) }; rootCommand.SetHandler(() => { }); @@ -126,21 +126,9 @@ public void Version_is_not_valid_with_other_tokens(string commandLine) .UseVersionOption() .Build(); - var console = new TestConsole(); - - var result = parser.Invoke(commandLine, console); + var result = parser.Parse(commandLine); - console.Out - .ToString() - .Should() - .NotContain(version); - - console.Error - .ToString() - .Should() - .Contain("--version option cannot be combined with other arguments."); - - result.Should().NotBe(0); + result.Errors.Should().Contain(e => e.Message == "--version option cannot be combined with other arguments."); } [Fact] @@ -206,7 +194,7 @@ public async Task Version_can_specify_additional_alias() [Fact] public void Version_is_not_valid_with_other_tokens_uses_custom_alias() { - var childCommand = new Command("subcommand"); + var childCommand = new Command("subcommand"); childCommand.SetHandler(() => { }); var rootCommand = new RootCommand { @@ -215,24 +203,12 @@ public void Version_is_not_valid_with_other_tokens_uses_custom_alias() rootCommand.SetHandler(() => { }); var parser = new CommandLineBuilder(rootCommand) - .UseVersionOption("-v") - .Build(); - - var console = new TestConsole(); - - var result = parser.Invoke("-v subcommand", console); - - console.Out - .ToString() - .Should() - .NotContain(version); + .UseVersionOption("-v") + .Build(); - console.Error - .ToString() - .Should() - .Contain("-v option cannot be combined with other arguments."); + var result = parser.Parse("-v subcommand"); - result.Should().NotBe(0); + result.Errors.Should().ContainSingle(e => e.Message == "-v option cannot be combined with other arguments."); } } } diff --git a/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs b/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs index 54039b04bb..fe49e34d0d 100644 --- a/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs +++ b/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs @@ -9,7 +9,6 @@ using System.CommandLine.Parsing; using System.IO; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using static System.Environment; @@ -22,24 +21,6 @@ namespace System.CommandLine /// public static class CommandLineBuilderExtensions { - private static readonly Lazy _assemblyVersion = - new(() => - { - var assembly = RootCommand.GetAssembly(); - - var assemblyVersionAttribute = assembly.GetCustomAttribute(); - - if (assemblyVersionAttribute is null) - { - return assembly.GetName().Version?.ToString() ?? ""; - } - else - { - return assemblyVersionAttribute.InformationalVersion; - } - - }); - /// /// Enables signaling and handling of process termination via a that can be passed to a during invocation. /// @@ -643,25 +624,6 @@ public static CommandLineBuilder UseVersionOption( builder.VersionOption = versionOption; builder.Command.Options.Add(versionOption); - builder.AddMiddleware(async (context, next) => - { - if (context.ParseResult.FindResultFor(versionOption) is { }) - { - if (context.ParseResult.Errors.Any(e => e.SymbolResult is OptionResult optionResult && optionResult.Option is VersionOption)) - { - context.InvocationResult = static ctx => ParseErrorResult.Apply(ctx, null); - } - else - { - context.Console.Out.WriteLine(_assemblyVersion.Value); - } - } - else - { - await next(context); - } - }, MiddlewareOrderInternal.VersionOption); - return builder; } @@ -684,25 +646,6 @@ public static CommandLineBuilder UseVersionOption( builder.VersionOption = versionOption; command.Options.Add(versionOption); - builder.AddMiddleware(async (context, next) => - { - if (context.ParseResult.FindResultFor(versionOption) is { }) - { - if (context.ParseResult.Errors.Any(e => e.SymbolResult is OptionResult optionResult && optionResult.Option is VersionOption)) - { - context.InvocationResult = static ctx => ParseErrorResult.Apply(ctx, null); - } - else - { - context.Console.Out.WriteLine(_assemblyVersion.Value); - } - } - else - { - await next(context); - } - }, MiddlewareOrderInternal.VersionOption); - return builder; } diff --git a/src/System.CommandLine/Help/HelpOption.cs b/src/System.CommandLine/Help/HelpOption.cs index b85d5abd95..616156cff1 100644 --- a/src/System.CommandLine/Help/HelpOption.cs +++ b/src/System.CommandLine/Help/HelpOption.cs @@ -12,7 +12,6 @@ public HelpOption(string[] aliases, Func getLocalizationR : base(aliases, null, new Argument { Arity = ArgumentArity.Zero }) { _localizationResources = getLocalizationResources; - DisallowBinding = true; } public HelpOption(Func getLocalizationResources) : this(new[] diff --git a/src/System.CommandLine/Help/VersionOption.cs b/src/System.CommandLine/Help/VersionOption.cs index c2238c627b..8e65328ee8 100644 --- a/src/System.CommandLine/Help/VersionOption.cs +++ b/src/System.CommandLine/Help/VersionOption.cs @@ -15,8 +15,6 @@ internal class VersionOption : Option { _builder = builder; - DisallowBinding = true; - AddValidators(); } @@ -24,8 +22,6 @@ public VersionOption(string[] aliases, CommandLineBuilder builder) : base(aliase { _builder = builder; - DisallowBinding = true; - AddValidators(); } diff --git a/src/System.CommandLine/Invocation/InvocationPipeline.cs b/src/System.CommandLine/Invocation/InvocationPipeline.cs index 4cbcfad873..a16b8a2832 100644 --- a/src/System.CommandLine/Invocation/InvocationPipeline.cs +++ b/src/System.CommandLine/Invocation/InvocationPipeline.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; +using System.CommandLine.Parsing; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -21,15 +22,15 @@ public Task InvokeAsync(IConsole? console = null, CancellationToken cancell { var context = new InvocationContext(_parseResult, console, cancellationToken); - if (context.Parser.Configuration.Middleware.Count == 0 - && context.ParseResult.CommandResult.Command.Handler is ICommandHandler handler) + if (context.Parser.Configuration.Middleware.Count == 0 && + _parseResult.Handler is not null) { - return handler.InvokeAsync(context); + return _parseResult.Handler.InvokeAsync(context); } - return FullInvocationChainAsync(context); + return InvokeHandlerWithMiddleware(context); - static async Task FullInvocationChainAsync(InvocationContext context) + static async Task InvokeHandlerWithMiddleware(InvocationContext context) { InvocationMiddleware invocationChain = BuildInvocationChain(context, true); @@ -49,9 +50,9 @@ public int Invoke(IConsole? console = null) return handler.Invoke(context); } - return FullInvocationChain(context); // kept in a separate method to avoid JITting + return InvokeHandlerWithMiddleware(context); // kept in a separate method to avoid JITting - static int FullInvocationChain(InvocationContext context) + static int InvokeHandlerWithMiddleware(InvocationContext context) { InvocationMiddleware invocationChain = BuildInvocationChain(context, false); @@ -68,19 +69,11 @@ private static InvocationMiddleware BuildInvocationChain(InvocationContext conte invocations.Add(async (invocationContext, _) => { - if (invocationContext - .ParseResult - .CommandResult - .Command is Command command) + if (invocationContext.ParseResult.Handler is { } handler) { - var handler = command.Handler; - - if (handler is not null) - { - context.ExitCode = invokeAsync - ? await handler.InvokeAsync(invocationContext) - : handler.Invoke(invocationContext); - } + context.ExitCode = invokeAsync + ? await handler.InvokeAsync(invocationContext) + : handler.Invoke(invocationContext); } }); @@ -88,7 +81,7 @@ private static InvocationMiddleware BuildInvocationChain(InvocationContext conte (first, second) => (ctx, next) => first(ctx, - c => second(c, next))); + c => second(c, next))); } private static int GetExitCode(InvocationContext context) diff --git a/src/System.CommandLine/Option.cs b/src/System.CommandLine/Option.cs index 02bb41e26b..45f3ebae59 100644 --- a/src/System.CommandLine/Option.cs +++ b/src/System.CommandLine/Option.cs @@ -77,8 +77,6 @@ public ArgumentArity Arity /// internal bool IsGlobal { get; set; } - internal bool DisallowBinding { get; init; } - /// /// Validators that will be called when the option is matched by the parser. /// diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index fb1fe74b92..08bd091071 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -19,6 +19,7 @@ public class ParseResult private readonly IReadOnlyList _unmatchedTokens; private Dictionary>? _directives; private CompletionContext? _completionContext; + private ICommandHandler? _handler; internal ParseResult( Parser parser, @@ -248,6 +249,25 @@ static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => .ToArray(); } + internal ICommandHandler? Handler + { + get + { + if (_handler is not null) + { + return _handler; + } + + if (CommandResult.Command is { } command) + { + return command.Handler; + } + + return null; + } + set => _handler = value; + } + private SymbolResult SymbolToComplete(int? position = null) { var commandResult = CommandResult; diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CommandResult.cs index 28e71fbda7..3b868952a2 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CommandResult.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; +using System.CommandLine.Help; namespace System.CommandLine.Parsing { @@ -83,7 +84,7 @@ private void ValidateOptions(bool completeValidation) { var option = options[i]; - if (!completeValidation && !(option.IsGlobal || option.Argument.HasDefaultValue || option.DisallowBinding)) + if (!completeValidation && !(option.IsGlobal || option.Argument.HasDefaultValue || (option is HelpOption or VersionOption))) { continue; } diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 472a75ed63..caf62a54cf 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.CommandLine.Help; +using System.CommandLine.Invocation; +using System.CommandLine.IO; namespace System.CommandLine.Parsing { @@ -18,6 +20,7 @@ internal sealed class ParseOperation private Dictionary>? _directives; private CommandResult _innermostCommandResult; private bool _isHelpRequested; + private bool _isVersionRequested; public ParseOperation( List tokens, @@ -59,7 +62,7 @@ internal ParseResult Parse(Parser parser) Validate(); } - return new( + ParseResult parseResult = new ( parser, _rootCommandResult, _innermostCommandResult, @@ -68,6 +71,17 @@ internal ParseResult Parse(Parser parser) _symbolResultTree.UnmatchedTokens, _symbolResultTree.Errors, _rawInput); + + if (_isVersionRequested) + { + // FIX: (GetResult) use the ActiveOption's handler + parseResult.Handler = new AnonymousCommandHandler(context => + { + context.Console.Out.WriteLine(RootCommand.ExecutableVersion); + }); + } + + return parseResult; } private void ParseSubcommand() @@ -173,10 +187,14 @@ private void ParseOption() if (!_symbolResultTree.TryGetValue(option, out SymbolResult? symbolResult)) { - if (option.DisallowBinding && option is HelpOption) + if (option is HelpOption) { _isHelpRequested = true; } + else if (option is VersionOption) + { + _isVersionRequested = true; + } optionResult = new OptionResult( option, diff --git a/src/System.CommandLine/RootCommand.cs b/src/System.CommandLine/RootCommand.cs index 3fe653c1cc..59443d33b5 100644 --- a/src/System.CommandLine/RootCommand.cs +++ b/src/System.CommandLine/RootCommand.cs @@ -19,6 +19,23 @@ public class RootCommand : Command private static Assembly? _assembly; private static string? _executablePath; private static string? _executableName; + private static string? _executableVersion; + + private static string GetExecutableVersion() + { + var assembly = GetAssembly(); + + var assemblyVersionAttribute = assembly.GetCustomAttribute(); + + if (assemblyVersionAttribute is null) + { + return assembly.GetName().Version?.ToString() ?? ""; + } + else + { + return assemblyVersionAttribute.InformationalVersion; + } + } /// The description of the command, shown in help. public RootCommand(string description = "") : base(ExecutableName, description) @@ -38,6 +55,8 @@ public static string ExecutableName /// The path to the currently running executable. /// public static string ExecutablePath => _executablePath ??= Environment.GetCommandLineArgs()[0]; + + internal static string ExecutableVersion => _executableVersion ??= GetExecutableVersion(); private protected override void RemoveAlias(string alias) {