diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt index 452160cab6..46238e35f8 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt @@ -146,7 +146,9 @@ System.CommandLine public static LocalizationResources Instance { get; } public System.String ArgumentConversionCannotParse(System.String value, System.Type expectedType) public System.String ArgumentConversionCannotParseForCommand(System.String value, System.String commandAlias, System.Type expectedType) + public System.String ArgumentConversionCannotParseForCommand(System.String value, System.String commandAlias, System.Type expectedType, System.Collections.Generic.IEnumerable completions) public System.String ArgumentConversionCannotParseForOption(System.String value, System.String optionAlias, System.Type expectedType) + public System.String ArgumentConversionCannotParseForOption(System.String value, System.String optionAlias, System.Type expectedType, System.Collections.Generic.IEnumerable completions) public System.String DirectoryDoesNotExist(System.String path) public System.String ErrorReadingResponseFile(System.String filePath, System.IO.IOException e) public System.String ExceptionHandlerHeader() @@ -386,6 +388,7 @@ System.CommandLine.IO System.CommandLine.Parsing public class ArgumentResult : SymbolResult public System.CommandLine.Argument Argument { get; } + public System.Void AddError(System.String errorMessage) public System.Object GetValueOrDefault() public T GetValueOrDefault() public System.Void OnlyTake(System.Int32 numberOfTokens) @@ -423,10 +426,10 @@ System.CommandLine.Parsing public static System.Threading.Tasks.Task InvokeAsync(this Parser parser, System.String[] args, System.CommandLine.IConsole console = null, System.Threading.CancellationToken cancellationToken = null) public static System.CommandLine.ParseResult Parse(this Parser parser, System.String commandLine) public abstract class SymbolResult - public System.String ErrorMessage { get; set; } public System.CommandLine.LocalizationResources LocalizationResources { get; } public SymbolResult Parent { get; } public System.Collections.Generic.IReadOnlyList Tokens { get; } + public System.Void AddError(System.String errorMessage) public ArgumentResult FindResultFor(System.CommandLine.Argument argument) public CommandResult FindResultFor(System.CommandLine.Command command) public OptionResult FindResultFor(System.CommandLine.Option option) diff --git a/src/System.CommandLine.NamingConventionBinder/ModelBinder.cs b/src/System.CommandLine.NamingConventionBinder/ModelBinder.cs index 56885c878a..08c52d510b 100644 --- a/src/System.CommandLine.NamingConventionBinder/ModelBinder.cs +++ b/src/System.CommandLine.NamingConventionBinder/ModelBinder.cs @@ -132,7 +132,7 @@ private bool ShortCutTheBinding() var valueSource = GetValueSource(bindingSources, bindingContext, ValueDescriptor, EnforceExplicitBinding); return bindingContext.TryBindToScalarValue(ValueDescriptor, valueSource, - bindingContext.ParseResult.CommandResult.LocalizationResources, + bindingContext.ParseResult, out var boundValue) ? (true, boundValue?.Value, true) : (false, null, false); @@ -277,7 +277,7 @@ internal static (BoundValue? boundValue, bool usedNonDefault) GetBoundValue( if (bindingContext.TryBindToScalarValue( valueDescriptor, valueSource, - bindingContext.ParseResult.CommandResult.LocalizationResources, + bindingContext.ParseResult, out var boundValue)) { return (boundValue, true); diff --git a/src/System.CommandLine.Tests/ArgumentTests.cs b/src/System.CommandLine.Tests/ArgumentTests.cs index 388224ec19..a32a0afa71 100644 --- a/src/System.CommandLine.Tests/ArgumentTests.cs +++ b/src/System.CommandLine.Tests/ArgumentTests.cs @@ -124,7 +124,7 @@ public void Validation_failure_message_can_be_specified_when_parsing_tokens() { var argument = new Argument(result => { - result.ErrorMessage = "oops!"; + result.AddError("oops!"); return null; }); @@ -143,7 +143,7 @@ public void Validation_failure_message_can_be_specified_when_evaluating_default_ { var argument = new Argument(result => { - result.ErrorMessage = "oops!"; + result.AddError("oops!"); return null; }, true); @@ -164,7 +164,7 @@ public void Validation_failure_message_can_be_specified_when_evaluating_default_ "-x", result => { - result.ErrorMessage = "oops!"; + result.AddError("oops!"); return null; }, true); @@ -389,7 +389,7 @@ public void Multiple_command_arguments_can_have_custom_parse_delegates() { new Argument("from", argumentResult => { - argumentResult.ErrorMessage = "nope"; + argumentResult.AddError("nope"); return null; }, true) { @@ -397,7 +397,7 @@ public void Multiple_command_arguments_can_have_custom_parse_delegates() }, new Argument("to", argumentResult => { - argumentResult.ErrorMessage = "UH UH"; + argumentResult.AddError("UH UH"); return null; }, true) { @@ -426,7 +426,7 @@ public void When_custom_conversion_fails_then_an_option_does_not_accept_further_ new Argument(), new Option("-x", argResult => { - argResult.ErrorMessage = "nope"; + argResult.AddError("nope"); return default; }) }; @@ -446,7 +446,7 @@ public void When_argument_cannot_be_parsed_as_the_specified_type_then_getting_va return value; } - argumentResult.ErrorMessage = $"'{argumentResult.Tokens.Single().Value}' is not an integer"; + argumentResult.AddError($"'{argumentResult.Tokens.Single().Value}' is not an integer"); return default; }); diff --git a/src/System.CommandLine.Tests/OptionTests.cs b/src/System.CommandLine.Tests/OptionTests.cs index 5716012e6d..f87e74c5c0 100644 --- a/src/System.CommandLine.Tests/OptionTests.cs +++ b/src/System.CommandLine.Tests/OptionTests.cs @@ -287,11 +287,11 @@ public void Option_T_default_value_is_validated() { var option = new Option("-x", () => 123); option.Validators.Add(symbol => - symbol.ErrorMessage = symbol.Tokens + symbol.AddError(symbol.Tokens .Select(t => t.Value) .Where(v => v == "123") .Select(_ => "ERR") - .FirstOrDefault()); + .First())); option .Parse("-x 123") diff --git a/src/System.CommandLine.Tests/ParseDiagramTests.cs b/src/System.CommandLine.Tests/ParseDiagramTests.cs index baaf6f6c32..2c78cab848 100644 --- a/src/System.CommandLine.Tests/ParseDiagramTests.cs +++ b/src/System.CommandLine.Tests/ParseDiagramTests.cs @@ -59,7 +59,7 @@ public void Parse_diagram_shows_type_conversion_errors() result.Diagram() .Should() - .Be($"[ {RootCommand.ExecutableName} [ -f ! ] ]"); + .Be($"[ {RootCommand.ExecutableName} ![ -f ] ]"); } [Fact] diff --git a/src/System.CommandLine.Tests/ParsingValidationTests.cs b/src/System.CommandLine.Tests/ParsingValidationTests.cs index 523e982b08..66ce605324 100644 --- a/src/System.CommandLine.Tests/ParsingValidationTests.cs +++ b/src/System.CommandLine.Tests/ParsingValidationTests.cs @@ -60,13 +60,17 @@ public void When_FromAmong_is_used_then_the_OptionResult_ErrorMessage_is_set() var parseResult = command.Parse("test --opt c"); - parseResult.FindResultFor(option) - .ErrorMessage - .Should() - .Be(parseResult.Errors.Single().Message) - .And - .Should() - .NotBeNull(); + var error = parseResult.Errors.Single(); + + error + .Message + .Should() + .Be(parseResult.CommandResult.LocalizationResources.UnrecognizedArgument("c", new []{ "a", "b"})); + error + .SymbolResult + .Should() + .BeOfType(); + } [Fact] // https://github.com/dotnet/command-line-api/issues/1475 @@ -79,13 +83,16 @@ public void When_FromAmong_is_used_then_the_ArgumentResult_ErrorMessage_is_set() var parseResult = command.Parse("test c"); - parseResult.FindResultFor(argument) - .ErrorMessage - .Should() - .Be(parseResult.Errors.Single().Message) - .And - .Should() - .NotBeNull(); + var error = parseResult.Errors.Single(); + + error + .Message + .Should() + .Be(parseResult.CommandResult.LocalizationResources.UnrecognizedArgument("c", new []{ "a", "b"})); + error + .SymbolResult + .Should() + .BeOfType(); } [Fact] // https://github.com/dotnet/command-line-api/issues/1556 @@ -171,7 +178,7 @@ public void When_FromAmong_is_used_for_multiple_arguments_and_invalid_input_is_p } [Fact] - public void When_FromAmong_is_used_and_multiple_invalid_inputs_are_provided_the_error_mentions_first_invalid_argument() + public void When_FromAmong_is_used_and_multiple_invalid_inputs_are_provided_the_errors_mention_all_invalid_arguments() { Option option = new(new[] { "--columns" }); option.AcceptOnlyFromAmong("author", "language", "tags", "type"); @@ -185,12 +192,17 @@ public void When_FromAmong_is_used_and_multiple_invalid_inputs_are_provided_the_ var result = command.Parse("list --columns c1 c2"); - // Currently there is no possibility for a single validator to produce multiple errors, - // so only the first one is checked. + result.Errors.Count.Should().Be(2); + result.Errors[0] - .Message - .Should() - .Be(LocalizationResources.Instance.UnrecognizedArgument("c1", new[] { "author", "language", "tags", "type" })); + .Message + .Should() + .Be(LocalizationResources.Instance.UnrecognizedArgument("c1", new[] { "author", "language", "tags", "type" })); + + result.Errors[1] + .Message + .Should() + .Be(LocalizationResources.Instance.UnrecognizedArgument("c2", new[] { "author", "language", "tags", "type" })); } [Fact] @@ -334,7 +346,7 @@ public void A_custom_validator_can_be_added_to_a_command() if (commandResult.Children.Any(sr => ((OptionResult)sr).Option.HasAlias("--one")) && commandResult.Children.Any(sr => ((OptionResult)sr).Option.HasAlias("--two"))) { - commandResult.ErrorMessage = "Options '--one' and '--two' cannot be used together."; + commandResult.AddError("Options '--one' and '--two' cannot be used together."); } }); @@ -358,7 +370,7 @@ public void A_custom_validator_can_be_added_to_an_option() { var value = r.GetValueOrDefault(); - r.ErrorMessage = $"Option {r.Token.Value} cannot be set to {value}"; + r.AddError($"Option {r.Token.Value} cannot be set to {value}"); }); var command = new RootCommand { option }; @@ -385,7 +397,7 @@ public void A_custom_validator_can_be_added_to_an_argument() { var value = r.GetValueOrDefault(); - r.ErrorMessage = $"Argument {r.Argument.Name} cannot be set to {value}"; + r.AddError($"Argument {r.Argument.Name} cannot be set to {value}"); }); var command = new RootCommand { argument }; @@ -449,7 +461,7 @@ public void Validators_on_global_options_are_executed_when_invoking_a_subcommand var option = new Option("--file"); option.Validators.Add(r => { - r.ErrorMessage = "Invoked validator"; + r.AddError("Invoked validator"); }); var subCommand = new Command("subcommand"); @@ -484,7 +496,7 @@ public async Task A_custom_validator_added_to_a_global_option_is_checked(string var handlerWasCalled = false; var globalOption = new Option("--value"); - globalOption.Validators.Add(r => r.ErrorMessage = "oops!"); + globalOption.Validators.Add(r => r.AddError("oops!")); var grandchildCommand = new Command("grandchild"); @@ -514,7 +526,7 @@ public void Custom_validator_error_messages_are_not_repeated() { var errorMessage = "that's not right..."; var argument = new Argument(); - argument.Validators.Add(r => r.ErrorMessage = errorMessage); + argument.Validators.Add(r => r.AddError(errorMessage)); var cmd = new Command("get") { @@ -541,7 +553,7 @@ public void The_parsed_value_of_an_argument_is_available_within_a_validator() if (value < 0 || value > 100) { - result.ErrorMessage = errorMessage; + result.AddError(errorMessage); } }); @@ -565,7 +577,7 @@ public void The_parsed_value_of_an_option_is_available_within_a_validator() if (value < 0 || value > 100) { - result.ErrorMessage = errorMessage; + result.AddError(errorMessage); } }); @@ -1196,7 +1208,7 @@ public void Arity_failures_are_not_reported_for_both_an_argument_and_its_parent_ public void Multiple_validators_on_the_same_command_do_not_report_duplicate_errors() { var command = new RootCommand(); - command.Validators.Add(result => result.ErrorMessage = "Wrong"); + command.Validators.Add(result => result.AddError("Wrong")); command.Validators.Add(_ => { }); var parseResult = command.Parse(""); @@ -1214,7 +1226,7 @@ public void Multiple_validators_on_the_same_command_do_not_report_duplicate_erro public void Multiple_validators_on_the_same_option_do_not_report_duplicate_errors() { var option = new Option("-x"); - option.Validators.Add(result => result.ErrorMessage = "Wrong"); + option.Validators.Add(result => result.AddError("Wrong")); option.Validators.Add(_ => { }); var command = new RootCommand @@ -1237,7 +1249,7 @@ public void Multiple_validators_on_the_same_option_do_not_report_duplicate_error public void Multiple_validators_on_the_same_argument_do_not_report_duplicate_errors() { var argument = new Argument(); - argument.Validators.Add(result => result.ErrorMessage = "Wrong"); + argument.Validators.Add(result => result.AddError("Wrong")); argument.Validators.Add(_ => { }); var command = new RootCommand @@ -1262,7 +1274,7 @@ internal void When_there_is_an_arity_error_then_further_errors_are_not_reported( var option = new Option("-o"); option.Validators.Add(result => { - result.ErrorMessage = "OOPS"; + result.AddError("OOPS"); }); //all good; var command = new Command("comm") diff --git a/src/System.CommandLine/Argument.cs b/src/System.CommandLine/Argument.cs index 42d5de1d7f..b9bf36e259 100644 --- a/src/System.CommandLine/Argument.cs +++ b/src/System.CommandLine/Argument.cs @@ -107,6 +107,8 @@ private protected override string DefaultName /// public List> Validators => _validators ??= new (); + internal bool HasValidators => (_validators?.Count ?? 0) > 0; + /// /// Gets the default value for the argument. /// diff --git a/src/System.CommandLine/ArgumentArity.cs b/src/System.CommandLine/ArgumentArity.cs index 6e3d496af5..a779882ad7 100644 --- a/src/System.CommandLine/ArgumentArity.cs +++ b/src/System.CommandLine/ArgumentArity.cs @@ -5,6 +5,7 @@ using System.CommandLine.Binding; using System.CommandLine.Parsing; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace System.CommandLine { @@ -72,48 +73,48 @@ public bool Equals(ArgumentArity other) => public override int GetHashCode() => MaximumNumberOfValues ^ MinimumNumberOfValues ^ IsNonDefault.GetHashCode(); - internal static ArgumentConversionResult? Validate( - SymbolResult symbolResult, - Argument argument, - int minimumNumberOfValues, - int maximumNumberOfValues) + internal static bool Validate(ArgumentResult argumentResult, [NotNullWhen(false)] out ArgumentConversionResult? error) { - var argumentResult = symbolResult switch - { - ArgumentResult a => a, - _ => symbolResult.FindResultFor(argument) - }; + error = null; - var tokenCount = argumentResult?.Tokens.Count ?? 0; + if (argumentResult.Parent is null || argumentResult.Parent is OptionResult { IsImplicit: true }) + { + return true; + } - if (tokenCount < minimumNumberOfValues) + int tokenCount = argumentResult.Tokens.Count; + if (tokenCount < argumentResult.Argument.Arity.MinimumNumberOfValues) { - if (symbolResult.UseDefaultValueFor(argument)) + if (argumentResult.Parent.UseDefaultValueFor(argumentResult)) { - return null; + return true; } - return ArgumentConversionResult.Failure( - argument, - symbolResult.LocalizationResources.RequiredArgumentMissing(symbolResult), + error = ArgumentConversionResult.Failure( + argumentResult, + argumentResult.LocalizationResources.RequiredArgumentMissing(argumentResult.Parent), ArgumentConversionResultType.FailedMissingArgument); + + return false; } - if (tokenCount > maximumNumberOfValues) + if (tokenCount > argumentResult.Argument.Arity.MaximumNumberOfValues) { - if (symbolResult is OptionResult optionResult) + if (argumentResult.Parent is OptionResult optionResult) { if (!optionResult.Option.AllowMultipleArgumentsPerToken) { - return ArgumentConversionResult.Failure( - argument, - symbolResult!.LocalizationResources.ExpectsOneArgument(symbolResult), + error = ArgumentConversionResult.Failure( + argumentResult, + argumentResult.LocalizationResources.ExpectsOneArgument(optionResult), ArgumentConversionResultType.FailedTooManyArguments); + + return false; } } } - return null; + return true; } /// diff --git a/src/System.CommandLine/Argument{T}.cs b/src/System.CommandLine/Argument{T}.cs index 35c3d4204d..ce51ca4156 100644 --- a/src/System.CommandLine/Argument{T}.cs +++ b/src/System.CommandLine/Argument{T}.cs @@ -92,9 +92,10 @@ public Argument( ConvertArguments = (ArgumentResult argumentResult, out object? value) => { + int errorsBefore = argumentResult.SymbolResultTree.ErrorCount; var result = parse(argumentResult); - if (string.IsNullOrEmpty(argumentResult.ErrorMessage)) + if (errorsBefore == argumentResult.SymbolResultTree.ErrorCount) { value = result; return true; @@ -194,8 +195,7 @@ void UnrecognizedArgumentError(ArgumentResult argumentResult) { if (Array.IndexOf(values, token.Value) < 0) { - argumentResult.ErrorMessage = argumentResult.LocalizationResources.UnrecognizedArgument(token.Value, values); - break; + argumentResult.AddError(argumentResult.LocalizationResources.UnrecognizedArgument(token.Value, values)); } } } @@ -221,7 +221,7 @@ public void AcceptLegalFilePathsOnly() if (invalidCharactersIndex >= 0) { - result.ErrorMessage = result.LocalizationResources.InvalidCharactersInPath(token.Value[invalidCharactersIndex]); + result.AddError(result.LocalizationResources.InvalidCharactersInPath(token.Value[invalidCharactersIndex])); } } }); @@ -244,7 +244,7 @@ public void AcceptLegalFileNamesOnly() if (invalidCharactersIndex >= 0) { - result.ErrorMessage = result.LocalizationResources.InvalidCharactersInFileName(token.Value[invalidCharactersIndex]); + result.AddError(result.LocalizationResources.InvalidCharactersInFileName(token.Value[invalidCharactersIndex])); } } }); diff --git a/src/System.CommandLine/Binding/ArgumentConversionResult.cs b/src/System.CommandLine/Binding/ArgumentConversionResult.cs index 2e0ac7b25f..6c103e1d3c 100644 --- a/src/System.CommandLine/Binding/ArgumentConversionResult.cs +++ b/src/System.CommandLine/Binding/ArgumentConversionResult.cs @@ -1,73 +1,83 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.CommandLine.Completions; +using System.CommandLine.Parsing; using System.Linq; namespace System.CommandLine.Binding { internal sealed class ArgumentConversionResult { - internal readonly Argument Argument; + internal readonly ArgumentResult ArgumentResult; internal readonly object? Value; internal readonly string? ErrorMessage; internal ArgumentConversionResultType Result; - private ArgumentConversionResult(Argument argument, string error, ArgumentConversionResultType failure) + private ArgumentConversionResult(ArgumentResult argumentResult, string error, ArgumentConversionResultType failure) { - Argument = argument ?? throw new ArgumentNullException(nameof(argument)); - ErrorMessage = error ?? throw new ArgumentNullException(nameof(error)); + ArgumentResult = argumentResult; + ErrorMessage = error; Result = failure; } - private ArgumentConversionResult(Argument argument, object? value) + private ArgumentConversionResult(ArgumentResult argumentResult, object? value, ArgumentConversionResultType result) { - Argument = argument ?? throw new ArgumentNullException(nameof(argument)); + ArgumentResult = argumentResult; Value = value; - Result = ArgumentConversionResultType.Successful; + Result = result; } - private ArgumentConversionResult(Argument argument) - { - Argument = argument ?? throw new ArgumentNullException(nameof(argument)); - Result = ArgumentConversionResultType.NoArgument; - } - - internal ArgumentConversionResult( - Argument argument, - Type expectedType, - string value, - LocalizationResources localizationResources) : - this(argument, FormatErrorMessage(argument, expectedType, value, localizationResources), ArgumentConversionResultType.FailedType) - { - } + internal static ArgumentConversionResult Failure(ArgumentResult argumentResult, string error, ArgumentConversionResultType reason) + => new(argumentResult, error, reason); - internal static ArgumentConversionResult Failure(Argument argument, string error, ArgumentConversionResultType reason) => new(argument, error, reason); + internal static ArgumentConversionResult ArgumentConversionCannotParse(ArgumentResult argumentResult, Type expectedType, string value) + => new(argumentResult, FormatErrorMessage(argumentResult, expectedType, value), ArgumentConversionResultType.FailedType); - public static ArgumentConversionResult Success(Argument argument, object? value) => new(argument, value); + public static ArgumentConversionResult Success(ArgumentResult argumentResult, object? value) + => new(argumentResult, value, ArgumentConversionResultType.Successful); - internal static ArgumentConversionResult None(Argument argument) => new(argument); + internal static ArgumentConversionResult None(ArgumentResult argumentResult) + => new(argumentResult, value: null, ArgumentConversionResultType.NoArgument); private static string FormatErrorMessage( - Argument argument, + ArgumentResult argumentResult, Type expectedType, - string value, - LocalizationResources localizationResources) + string value) { - if (argument.FirstParent?.Symbol is IdentifierSymbol identifierSymbol && - argument.FirstParent.Next is null) + if (argumentResult.Parent is CommandResult commandResult) { - var alias = identifierSymbol.GetLongestAlias(removePrefix: false); + string alias = commandResult.Command.GetLongestAlias(removePrefix: false); + CompletionItem[] completionItems = argumentResult.Argument.GetCompletions(CompletionContext.Empty).ToArray(); - switch (identifierSymbol) + if (completionItems.Length > 0) + { + return argumentResult.LocalizationResources.ArgumentConversionCannotParseForCommand( + value, alias, expectedType, completionItems.Select(ci => ci.Label)); + } + else + { + return argumentResult.LocalizationResources.ArgumentConversionCannotParseForCommand(value, alias, expectedType); + } + } + else if (argumentResult.Parent is OptionResult optionResult) + { + string alias = optionResult.Option.GetLongestAlias(removePrefix: false); + CompletionItem[] completionItems = optionResult.Option.GetCompletions(CompletionContext.Empty).ToArray(); + + if (completionItems.Length > 0) + { + return argumentResult.LocalizationResources.ArgumentConversionCannotParseForOption( + value, alias, expectedType, completionItems.Select(ci => ci.Label)); + } + else { - case Command _: - return localizationResources.ArgumentConversionCannotParseForCommand(value, alias, expectedType); - case Option _: - return localizationResources.ArgumentConversionCannotParseForOption(value, alias, expectedType); + return argumentResult.LocalizationResources.ArgumentConversionCannotParseForOption(value, alias, expectedType); } } - return localizationResources.ArgumentConversionCannotParse(value, expectedType); + // fake ArgumentResults with no Parent + return argumentResult.LocalizationResources.ArgumentConversionCannotParse(value, expectedType); } } } \ No newline at end of file diff --git a/src/System.CommandLine/Binding/ArgumentConverter.cs b/src/System.CommandLine/Binding/ArgumentConverter.cs index bc86831770..c982a66fdf 100644 --- a/src/System.CommandLine/Binding/ArgumentConverter.cs +++ b/src/System.CommandLine/Binding/ArgumentConverter.cs @@ -11,70 +11,72 @@ namespace System.CommandLine.Binding internal static partial class ArgumentConverter { internal static ArgumentConversionResult ConvertObject( - Argument argument, + ArgumentResult argumentResult, Type type, - object? value, - LocalizationResources localizationResources) + object? value) { switch (value) { case Token singleValue: - return ConvertToken(argument, type, singleValue, localizationResources); + return ConvertToken(argumentResult, type, singleValue); case IReadOnlyList manyValues: - return ConvertTokens(argument, type, manyValues, localizationResources); + return ConvertTokens(argumentResult, type, manyValues); default: - return None(argument); + return None(argumentResult); } } private static ArgumentConversionResult ConvertToken( - Argument argument, + ArgumentResult argumentResult, Type type, - Token token, - LocalizationResources localizationResources) + Token token) { var value = token.Value; if (type.TryGetNullableType(out var nullableType)) { - return ConvertToken(argument, nullableType, token, localizationResources); + return ConvertToken(argumentResult, nullableType, token); } if (_stringConverters.TryGetValue(type, out var tryConvert)) { if (tryConvert(value, out var converted)) { - return Success(argument, converted); + return Success(argumentResult, converted); } else { - return Failure(argument, type, value, localizationResources); + return ArgumentConversionCannotParse(argumentResult, type, value); } } if (type.IsEnum) { +#if NET7_0_OR_GREATER + if (Enum.TryParse(type, value, ignoreCase: true, out var converted)) + { + return Success(argumentResult, converted); + } +#else try { - return Success(argument, Enum.Parse(type, value, true)); + return Success(argumentResult, Enum.Parse(type, value, true)); } catch (ArgumentException) { - // TODO: find a way to do this without the try..catch } +#endif } - return Failure(argument, type, value, localizationResources); + return ArgumentConversionCannotParse(argumentResult, type, value); } private static ArgumentConversionResult ConvertTokens( - Argument argument, + ArgumentResult argumentResult, Type type, - IReadOnlyList tokens, - LocalizationResources localizationResources, - ArgumentResult? argumentResult = null) + IReadOnlyList tokens) { var itemType = type.GetElementTypeIfEnumerable() ?? typeof(string); var values = CreateEnumerable(type, itemType, tokens.Count); @@ -84,7 +86,7 @@ private static ArgumentConversionResult ConvertTokens( { var token = tokens[i]; - var result = ConvertToken(argument, itemType, token, localizationResources); + var result = ConvertToken(argumentResult, itemType, token); switch (result.Result) { @@ -101,7 +103,7 @@ private static ArgumentConversionResult ConvertTokens( break; default: // failures - if (argumentResult is { Parent: CommandResult }) + if (argumentResult.Parent is CommandResult) { argumentResult.OnlyTake(i); @@ -113,7 +115,7 @@ private static ArgumentConversionResult ConvertTokens( } } - return Success(argument, values); + return Success(argumentResult, values); } internal static TryConvertArgument? GetConverter(Argument argument) @@ -167,35 +169,26 @@ private static bool CanBeBoundFromScalarValue(this Type type) } } - private static ArgumentConversionResult Failure( - Argument argument, - Type expectedType, - string value, - LocalizationResources localizationResources) - { - return new ArgumentConversionResult(argument, expectedType, value, localizationResources); - } - internal static ArgumentConversionResult ConvertIfNeeded( this ArgumentConversionResult conversionResult, - SymbolResult symbolResult, Type toType) { return conversionResult.Result switch { ArgumentConversionResultType.Successful when !toType.IsInstanceOfType(conversionResult.Value) => - ConvertObject(conversionResult.Argument, + ConvertObject(conversionResult.ArgumentResult, toType, - conversionResult.Value, - symbolResult.LocalizationResources), + conversionResult.Value), - ArgumentConversionResultType.NoArgument when conversionResult.Argument.ValueType == typeof(bool) || conversionResult.Argument.ValueType == typeof(bool?) => - Success(conversionResult.Argument, true), + ArgumentConversionResultType.NoArgument when conversionResult.ArgumentResult.Argument.ValueType == typeof(bool) => + Success(conversionResult.ArgumentResult, true), + ArgumentConversionResultType.NoArgument when conversionResult.ArgumentResult.Argument.ValueType == typeof(bool?) => + Success(conversionResult.ArgumentResult, true), - ArgumentConversionResultType.NoArgument when conversionResult.Argument.Arity.MinimumNumberOfValues > 0 => + ArgumentConversionResultType.NoArgument when conversionResult.ArgumentResult.Argument.Arity.MinimumNumberOfValues > 0 => ArgumentConversionResult.Failure( - conversionResult.Argument, - symbolResult.LocalizationResources.RequiredArgumentMissing(symbolResult), + conversionResult.ArgumentResult, + conversionResult.ArgumentResult.LocalizationResources.RequiredArgumentMissing(conversionResult.ArgumentResult.Parent!), ArgumentConversionResultType.FailedMissingArgument), _ => conversionResult @@ -219,18 +212,15 @@ public static bool TryConvertArgument(ArgumentResult argumentResult, out object? ArgumentConversionResult result = argument.Arity.MaximumNumberOfValues switch { // 0 is an implicit bool, i.e. a "flag" - 0 => Success(argumentResult.Argument, true), - 1 => ConvertObject(argument, + 0 => Success(argumentResult, true), + 1 => ConvertObject(argumentResult, argument.ValueType, argumentResult.Tokens.Count > 0 ? argumentResult.Tokens[argumentResult.Tokens.Count - 1] - : null, - argumentResult.LocalizationResources), - _ => ConvertTokens(argument, + : null), + _ => ConvertTokens(argumentResult, argument.ValueType, - argumentResult.Tokens, - argumentResult.LocalizationResources, - argumentResult) + argumentResult.Tokens) }; value = result; diff --git a/src/System.CommandLine/Binding/BindingContext.cs b/src/System.CommandLine/Binding/BindingContext.cs index 13d3bd6e4b..435decc4f2 100644 --- a/src/System.CommandLine/Binding/BindingContext.cs +++ b/src/System.CommandLine/Binding/BindingContext.cs @@ -3,6 +3,7 @@ using System.CommandLine.Help; using System.CommandLine.Invocation; +using System.CommandLine.Parsing; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -84,7 +85,7 @@ internal bool TryGetValueSource( internal bool TryBindToScalarValue( IValueDescriptor valueDescriptor, IValueSource valueSource, - LocalizationResources localizationResources, + ParseResult parseResult, out BoundValue? boundValue) { if (valueSource.TryGetValue(valueDescriptor, this, out var value)) @@ -96,11 +97,16 @@ internal bool TryBindToScalarValue( } else { + ArgumentResult argumentResult = valueDescriptor is Argument argument + ? parseResult.FindResultFor(argument) is ArgumentResult found + ? found + : new ArgumentResult(argument, parseResult.RootCommandResult.SymbolResultTree, null) + : new ArgumentResult(new Argument(valueDescriptor.ValueName), parseResult.RootCommandResult.SymbolResultTree, null); + var parsed = ArgumentConverter.ConvertObject( - valueDescriptor as Argument ?? new Argument(valueDescriptor.ValueName), + argumentResult, valueDescriptor.ValueType, - value, - localizationResources); + value); if (parsed.Result == ArgumentConversionResultType.Successful) { diff --git a/src/System.CommandLine/Help/VersionOption.cs b/src/System.CommandLine/Help/VersionOption.cs index 55b4d9e9c0..c2238c627b 100644 --- a/src/System.CommandLine/Help/VersionOption.cs +++ b/src/System.CommandLine/Help/VersionOption.cs @@ -37,7 +37,7 @@ private void AddValidators() parent.Children.Where(r => !(r is OptionResult optionResult && optionResult.Option is VersionOption)) .Any(IsNotImplicit)) { - result.ErrorMessage = result.LocalizationResources.VersionOptionCannotBeCombinedWithOtherArguments(result.Token?.Value ?? result.Option.Name); + result.AddError(result.LocalizationResources.VersionOptionCannotBeCombinedWithOtherArguments(result.Token?.Value ?? result.Option.Name)); } }); } diff --git a/src/System.CommandLine/LocalizationResources.cs b/src/System.CommandLine/LocalizationResources.cs index 45cc33b8de..ce1c832e31 100644 --- a/src/System.CommandLine/LocalizationResources.cs +++ b/src/System.CommandLine/LocalizationResources.cs @@ -242,12 +242,26 @@ public virtual string ArgumentConversionCannotParse(string value, Type expectedT public virtual string ArgumentConversionCannotParseForCommand(string value, string commandAlias, Type expectedType) => GetResourceString(Properties.Resources.ArgumentConversionCannotParseForCommand, value, commandAlias, expectedType); + /// + /// Interpolates values into a localized string similar to Cannot parse argument '{0}' for command '{1}' as expected type {2}.. + /// + public virtual string ArgumentConversionCannotParseForCommand(string value, string commandAlias, Type expectedType, IEnumerable completions) + => GetResourceString(Properties.Resources.ArgumentConversionCannotParseForCommand_Completions, + value, commandAlias, expectedType, Environment.NewLine + string.Join(Environment.NewLine, completions)); + /// /// Interpolates values into a localized string similar to Cannot parse argument '{0}' for option '{1}' as expected type {2}.. /// public virtual string ArgumentConversionCannotParseForOption(string value, string optionAlias, Type expectedType) => GetResourceString(Properties.Resources.ArgumentConversionCannotParseForOption, value, optionAlias, expectedType); + /// + /// Interpolates values into a localized string similar to Cannot parse argument '{0}' for option '{1}' as expected type {2}.. + /// + public virtual string ArgumentConversionCannotParseForOption(string value, string optionAlias, Type expectedType, IEnumerable completions) + => GetResourceString(Properties.Resources.ArgumentConversionCannotParseForOption_Completions, + value, optionAlias, expectedType, Environment.NewLine + string.Join(Environment.NewLine, completions)); + /// /// Interpolates values into a localized string. /// diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index dca48e3457..fb1fe74b92 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -241,8 +241,8 @@ currentSymbol is not null static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => commandResult .Children - .Where(c => c.IsArgumentLimitReached) .OfType() + .Where(c => c.IsArgumentLimitReached) .Select(o => o.Option) .SelectMany(c => c.Aliases) .ToArray(); diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/ArgumentResult.cs index dfe89c4cb4..c77bacdecd 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/ArgumentResult.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Collections.Generic; using System.CommandLine.Binding; using System.Linq; @@ -13,6 +12,7 @@ namespace System.CommandLine.Parsing public sealed class ArgumentResult : SymbolResult { private ArgumentConversionResult? _conversionResult; + private bool _onlyTakeHasBeenCalled; internal ArgumentResult( Argument argument, @@ -27,14 +27,12 @@ internal ArgumentResult( /// public Argument Argument { get; } - internal override int MaximumArgumentCapacity => Argument.Arity.MaximumNumberOfValues; + internal bool IsArgumentLimitReached => Argument.Arity.MaximumNumberOfValues == (_tokens?.Count ?? 0); internal bool IsImplicit => Argument.HasDefaultValue && Tokens.Count == 0; - internal IReadOnlyList? PassedOnTokens { get; private set; } - internal ArgumentConversionResult GetArgumentConversionResult() => - _conversionResult ??= Convert(Argument); + _conversionResult ??= ValidateAndConvert(useValidators: true); /// public object? GetValueOrDefault() => @@ -45,8 +43,8 @@ internal ArgumentConversionResult GetArgumentConversionResult() => /// /// The parsed value or the default value for public T GetValueOrDefault() => - GetArgumentConversionResult() - .ConvertIfNeeded(this, typeof(T)) + (_conversionResult ??= ValidateAndConvert(useValidators: false)) + .ConvertIfNeeded(typeof(T)) .GetValueOrDefault(); /// @@ -55,6 +53,7 @@ public T GetValueOrDefault() => /// The number of tokens to take. The rest are passed on. /// numberOfTokens - Value must be at least 1. /// Thrown if this method is called more than once. + /// Thrown if this method is called by Option-owned ArgumentResult. public void OnlyTake(int numberOfTokens) { if (numberOfTokens < 0) @@ -62,108 +61,148 @@ public void OnlyTake(int numberOfTokens) throw new ArgumentOutOfRangeException(nameof(numberOfTokens), numberOfTokens, "Value must be at least 1."); } - if (PassedOnTokens is { }) + if (_onlyTakeHasBeenCalled) { throw new InvalidOperationException($"{nameof(OnlyTake)} can only be called once."); } - if (_tokens is not null) + if (Parent is OptionResult) { - var passedOnTokensCount = _tokens.Count - numberOfTokens; + throw new NotSupportedException($"{nameof(OnlyTake)} is supported only for a {nameof(Command)}-owned {nameof(ArgumentResult)}"); + } - PassedOnTokens = new List(_tokens.GetRange(numberOfTokens, passedOnTokensCount)); + _onlyTakeHasBeenCalled = true; - _tokens.RemoveRange(numberOfTokens, passedOnTokensCount); + if (_tokens is null || numberOfTokens >= _tokens.Count) + { + return; } - } - /// - public override string ToString() => $"{GetType().Name} {Argument.Name}: {string.Join(" ", Tokens.Select(t => $"<{t.Value}>"))}"; + CommandResult parent = (CommandResult)Parent!; + var arguments = parent.Command.Arguments; + int argumentIndex = arguments.IndexOf(Argument); + int nextArgumentIndex = argumentIndex + 1; + int tokensToPass = _tokens.Count - numberOfTokens; - internal ParseError? CustomError(Argument argument) - { - for (var i = 0; i < argument.Validators.Count; i++) + while (tokensToPass > 0 && nextArgumentIndex < arguments.Count) { - var validateSymbolResult = argument.Validators[i]; - validateSymbolResult(this); + Argument nextArgument = parent.Command.Arguments[nextArgumentIndex]; + ArgumentResult nextArgumentResult; + + if (SymbolResultTree.TryGetValue(nextArgument, out SymbolResult? symbolResult)) + { + nextArgumentResult = (ArgumentResult)symbolResult; + } + else + { + // it might have not been parsed yet or due too few arguments, so we add it now + nextArgumentResult = new ArgumentResult(nextArgument, SymbolResultTree, Parent); + SymbolResultTree.Add(nextArgument, nextArgumentResult); + } - if (!string.IsNullOrWhiteSpace(ErrorMessage)) + while (!nextArgumentResult.IsArgumentLimitReached && tokensToPass > 0) { - return new ParseError(ErrorMessage!, Parent is OptionResult option ? option : this); + Token toPass = _tokens[numberOfTokens]; + _tokens.RemoveAt(numberOfTokens); + nextArgumentResult.AddToken(toPass); + --tokensToPass; } + + nextArgumentIndex++; + } + + // When_tokens_are_passed_on_by_custom_parser_on_last_argument_then_they_become_unmatched_tokens + while (tokensToPass > 0) + { + Token unmatched = _tokens[numberOfTokens]; + _tokens.RemoveAt(numberOfTokens); + SymbolResultTree.AddUnmatchedToken(unmatched); + --tokensToPass; } + } + + /// + public override string ToString() => $"{GetType().Name} {Argument.Name}: {string.Join(" ", Tokens.Select(t => $"<{t.Value}>"))}"; - return null; + /// + public override void AddError(string errorMessage) + { + SymbolResultTree.AddError(new ParseError(errorMessage, Parent is OptionResult option ? option : this)); + _conversionResult = ArgumentConversionResult.Failure(this, errorMessage, ArgumentConversionResultType.Failed); } - private ArgumentConversionResult Convert(Argument argument) + private ArgumentConversionResult ValidateAndConvert(bool useValidators) { - if (ShouldCheckArity() && - Parent is { } && - ArgumentArity.Validate( - Parent, - argument, - argument.Arity.MinimumNumberOfValues, - argument.Arity.MaximumNumberOfValues) is { } failed) // returns null on success + if (!ArgumentArity.Validate(this, out ArgumentConversionResult? arityFailure)) { - return failed; + return ReportErrorIfNeeded(arityFailure); } - if (Parent!.UseDefaultValueFor(argument)) + // There is nothing that stops user-defined Validator from calling ArgumentResult.GetValueOrDefault. + // In such cases, we can't call the validators again, as it would create infinite recursion. + // GetArgumentConversionResult => ValidateAndConvert => Validator + // => GetValueOrDefault => ValidateAndConvert (again) + if (useValidators && Argument.HasValidators) { - var argumentResult = new ArgumentResult(argument, SymbolResultTree, Parent); - - var defaultValue = argument.GetDefaultValue(argumentResult); - - if (string.IsNullOrEmpty(argumentResult.ErrorMessage)) + for (var i = 0; i < Argument.Validators.Count; i++) { - return ArgumentConversionResult.Success( - argument, - defaultValue); + Argument.Validators[i](this); } - else + + // validator provided by the user might report an error, which sets _conversionResult + if (_conversionResult is not null) { - return ArgumentConversionResult.Failure( - argument, - argumentResult.ErrorMessage!, - ArgumentConversionResultType.Failed); + return _conversionResult; } } - if (argument.ConvertArguments is null) + if (Parent!.UseDefaultValueFor(this)) + { + var defaultValue = Argument.GetDefaultValue(this); + + // default value factory provided by the user might report an error, which sets _conversionResult + return _conversionResult ?? ArgumentConversionResult.Success(this, defaultValue); + } + + if (Argument.ConvertArguments is null) { - return argument.Arity.MaximumNumberOfValues switch + return Argument.Arity.MaximumNumberOfValues switch { - 1 => ArgumentConversionResult.Success(argument, Tokens.SingleOrDefault()), - _ => ArgumentConversionResult.Success(argument, Tokens) + 1 when _tokens is null => ArgumentConversionResult.None(this), + 1 when _tokens is not null => ArgumentConversionResult.Success(this, _tokens[0]), + _ => ArgumentConversionResult.Success(this, Tokens) }; } - var success = argument.ConvertArguments(this, out var value); + var success = Argument.ConvertArguments(this, out var value); - if (value is ArgumentConversionResult conversionResult) + // default value factory provided by the user might report an error, which sets _conversionResult + if (_conversionResult is not null) { - return conversionResult; + return _conversionResult; } - if (success) + if (value is ArgumentConversionResult conversionResult) { - return ArgumentConversionResult.Success(argument, value); + return ReportErrorIfNeeded(conversionResult); } - if (ErrorMessage is not null) + if (success) { - return ArgumentConversionResult.Failure(argument, ErrorMessage, ArgumentConversionResultType.Failed); + return ArgumentConversionResult.Success(this, value); } - return new ArgumentConversionResult( - argument, - argument.ValueType, - Tokens[0].Value, - LocalizationResources); + return ReportErrorIfNeeded(ArgumentConversionResult.ArgumentConversionCannotParse(this, Argument.ValueType, Tokens[0].Value)); - bool ShouldCheckArity() => - Parent is not OptionResult { IsImplicit: true }; + ArgumentConversionResult ReportErrorIfNeeded(ArgumentConversionResult result) + { + if (result.Result >= ArgumentConversionResultType.Failed) + { + SymbolResultTree.AddError(new ParseError(result.ErrorMessage!, Parent is OptionResult option ? option : this)); + } + + return result; + } } } } diff --git a/src/System.CommandLine/Parsing/CommandArgumentNode.cs b/src/System.CommandLine/Parsing/CommandArgumentNode.cs deleted file mode 100644 index 7c21746ff5..0000000000 --- a/src/System.CommandLine/Parsing/CommandArgumentNode.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Diagnostics; - -namespace System.CommandLine.Parsing -{ - internal sealed class CommandArgumentNode : SyntaxNode - { - public CommandArgumentNode( - Token token, - Argument argument, - CommandNode parent) : base(token) - { - Debug.Assert(token.Type == TokenType.Argument, $"Incorrect token type: {token}"); - - Argument = argument; - ParentCommandNode = parent; - } - - public Argument Argument { get; } - - public CommandNode ParentCommandNode { get; } - } -} diff --git a/src/System.CommandLine/Parsing/CommandNode.cs b/src/System.CommandLine/Parsing/CommandNode.cs deleted file mode 100644 index 63eeb9b589..0000000000 --- a/src/System.CommandLine/Parsing/CommandNode.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace System.CommandLine.Parsing -{ - internal sealed class CommandNode : NonterminalSyntaxNode - { - public CommandNode( - Token token, - Command command) : base(token) - { - Command = command; - } - - public Command Command { get; } - } -} diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CommandResult.cs index 645e7d0388..28e71fbda7 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CommandResult.cs @@ -36,32 +36,147 @@ internal CommandResult( /// public IEnumerable Children => SymbolResultTree.GetChildren(this); - internal sealed override int MaximumArgumentCapacity + internal override bool UseDefaultValueFor(ArgumentResult argumentResult) + => argumentResult.Argument.HasDefaultValue && argumentResult.Tokens.Count == 0; + + /// Only the inner most command goes through complete validation. + internal void Validate(bool completeValidation) + { + if (completeValidation) + { + if (Command.Handler is null && Command.HasSubcommands) + { + SymbolResultTree.InsertFirstError( + new ParseError(LocalizationResources.RequiredCommandWasNotProvided(), this)); + } + + if (Command.HasValidators) + { + int errorCountBefore = SymbolResultTree.ErrorCount; + for (var i = 0; i < Command.Validators.Count; i++) + { + Command.Validators[i](this); + } + + if (SymbolResultTree.ErrorCount != errorCountBefore) + { + return; + } + } + } + + if (Command.HasOptions) + { + ValidateOptions(completeValidation); + } + + if (Command.HasArguments) + { + ValidateArguments(completeValidation); + } + } + + private void ValidateOptions(bool completeValidation) { - get + var options = Command.Options; + for (var i = 0; i < options.Count; i++) { - var value = 0; + var option = options[i]; + + if (!completeValidation && !(option.IsGlobal || option.Argument.HasDefaultValue || option.DisallowBinding)) + { + continue; + } + + OptionResult optionResult; + ArgumentResult argumentResult; + + if (!SymbolResultTree.TryGetValue(option, out SymbolResult? symbolResult)) + { + if (option.IsRequired) + { + AddError(LocalizationResources.RequiredOptionWasNotProvided(option)); + continue; + } + else if (option.Argument.HasDefaultValue) + { + optionResult = new(option, SymbolResultTree, null, this); + SymbolResultTree.Add(optionResult.Option, optionResult); - if (Command.HasArguments) + argumentResult = new(optionResult.Option.Argument, SymbolResultTree, optionResult); + SymbolResultTree.Add(optionResult.Option.Argument, argumentResult); + } + else + { + continue; + } + } + else { - var arguments = Command.Arguments; + optionResult = (OptionResult)symbolResult; + argumentResult = (ArgumentResult)SymbolResultTree[option.Argument]; + } - for (var i = 0; i < arguments.Count; i++) + // When_there_is_an_arity_error_then_further_errors_are_not_reported + if (!ArgumentArity.Validate(argumentResult, out var error)) + { + optionResult.AddError(error.ErrorMessage!); + continue; + } + + if (optionResult.Option.HasValidators) + { + int errorsBefore = SymbolResultTree.ErrorCount; + + for (var j = 0; j < optionResult.Option.Validators.Count; j++) { - value += arguments[i].Arity.MaximumNumberOfValues; + optionResult.Option.Validators[j](optionResult); + } + + if (errorsBefore != SymbolResultTree.ErrorCount) + { + break; } } - return value; + _ = argumentResult.GetArgumentConversionResult(); } } - internal override bool UseDefaultValueFor(Argument argument) => - FindResultFor(argument) switch + private void ValidateArguments(bool completeValidation) + { + var arguments = Command.Arguments; + for (var i = 0; i < arguments.Count; i++) { - ArgumentResult arg => arg.Argument.HasDefaultValue && - arg.Tokens.Count == 0, - _ => false - }; + Argument argument = arguments[i]; + + if (!completeValidation && !argument.HasDefaultValue) + { + continue; + } + + ArgumentResult? argumentResult; + if (SymbolResultTree.TryGetValue(argument, out SymbolResult? symbolResult)) + { + argumentResult = (ArgumentResult)symbolResult; + } + else if (argument.HasDefaultValue) + { + argumentResult = new ArgumentResult(argument, SymbolResultTree, this); + SymbolResultTree[argument] = argumentResult; + } + else if (argument.Arity.MinimumNumberOfValues > 0) + { + AddError(LocalizationResources.RequiredArgumentMissing(this)); + continue; + } + else + { + continue; + } + + _ = argumentResult.GetArgumentConversionResult(); + } + } } } diff --git a/src/System.CommandLine/Parsing/DirectiveNode.cs b/src/System.CommandLine/Parsing/DirectiveNode.cs deleted file mode 100644 index c747212378..0000000000 --- a/src/System.CommandLine/Parsing/DirectiveNode.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Diagnostics; - -namespace System.CommandLine.Parsing -{ - internal sealed class DirectiveNode : SyntaxNode - { - public DirectiveNode( - Token token, - string name, - string? value) : base(token) - { - Debug.Assert(token.Type == TokenType.Directive, $"Incorrect token type: {token}"); - - Name = name; - Value = value; - } - - public string Name { get; } - - public string? Value { get; } - } -} diff --git a/src/System.CommandLine/Parsing/NonterminalSyntaxNode.cs b/src/System.CommandLine/Parsing/NonterminalSyntaxNode.cs deleted file mode 100644 index dcf9dd833d..0000000000 --- a/src/System.CommandLine/Parsing/NonterminalSyntaxNode.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Generic; - -namespace System.CommandLine.Parsing -{ - internal abstract class NonterminalSyntaxNode : SyntaxNode - { - private List? _children; - - protected NonterminalSyntaxNode(Token token) : base(token) - { - } - - public IReadOnlyList? Children => _children; - - internal void AddChildNode(SyntaxNode node) => (_children ??= new()).Add(node); - } -} diff --git a/src/System.CommandLine/Parsing/OptionArgumentNode.cs b/src/System.CommandLine/Parsing/OptionArgumentNode.cs deleted file mode 100644 index f9452cd514..0000000000 --- a/src/System.CommandLine/Parsing/OptionArgumentNode.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Diagnostics; - -namespace System.CommandLine.Parsing -{ - internal sealed class OptionArgumentNode : SyntaxNode - { - public OptionArgumentNode( - Token token, - Argument argument, - OptionNode parent) : base(token) - { - Debug.Assert(token.Type == TokenType.Argument, $"Incorrect token type: {token}"); - - Argument = argument; - ParentOptionNode = parent; - } - - public Argument Argument { get; } - - public OptionNode ParentOptionNode { get; } - } -} diff --git a/src/System.CommandLine/Parsing/OptionNode.cs b/src/System.CommandLine/Parsing/OptionNode.cs deleted file mode 100644 index 25ca29023b..0000000000 --- a/src/System.CommandLine/Parsing/OptionNode.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace System.CommandLine.Parsing -{ - internal sealed class OptionNode : NonterminalSyntaxNode - { - public OptionNode( - Token token, - Option option) : base(token) - { - Option = option; - } - - public Option Option { get; } - } -} \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/OptionResult.cs index acbe7db56d..3f540cdfb5 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/OptionResult.cs @@ -40,8 +40,6 @@ internal OptionResult( /// public Token? Token { get; } - internal override int MaximumArgumentCapacity => Option.Argument.Arity.MaximumNumberOfValues; - /// public object? GetValueOrDefault() => Option.ValueType == typeof(bool) @@ -54,42 +52,15 @@ internal OptionResult( /// The parsed value or the default value for [return: MaybeNull] public T GetValueOrDefault() => - this.ConvertIfNeeded(typeof(T)) + ArgumentConversionResult.ConvertIfNeeded(typeof(T)) .GetValueOrDefault(); - private protected override int RemainingArgumentCapacity - { - get - { - var capacity = base.RemainingArgumentCapacity; - - if (IsImplicit && capacity < int.MaxValue) - { - capacity += 1; - } - - return capacity; - } - } + internal bool IsArgumentLimitReached + => Option.Argument.Arity.MaximumNumberOfValues == (IsImplicit ? Tokens.Count - 1 : Tokens.Count); internal ArgumentConversionResult ArgumentConversionResult - { - get - { - if (_argumentConversionResult is null) - { - if (FindResultFor(Option.Argument) is ArgumentResult firstChild) - { - return _argumentConversionResult = firstChild.GetArgumentConversionResult(); - } - - return _argumentConversionResult = ArgumentConversionResult.None(Option.Argument); - } - - return _argumentConversionResult; - } - } + => _argumentConversionResult ??= FindResultFor(Option.Argument)!.GetArgumentConversionResult(); - internal override bool UseDefaultValueFor(Argument argument) => IsImplicit; + internal override bool UseDefaultValueFor(ArgumentResult argument) => IsImplicit; } } diff --git a/src/System.CommandLine/Parsing/OptionResultExtensions.cs b/src/System.CommandLine/Parsing/OptionResultExtensions.cs deleted file mode 100644 index 2ab289a32f..0000000000 --- a/src/System.CommandLine/Parsing/OptionResultExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.CommandLine.Binding; - -namespace System.CommandLine.Parsing -{ - internal static class OptionResultExtensions - { - internal static ArgumentConversionResult ConvertIfNeeded( - this OptionResult optionResult, - Type type) => - optionResult.ArgumentConversionResult - .ConvertIfNeeded(optionResult, type); - } -} \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index da2381f23b..5988dbc5c2 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.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 { @@ -9,22 +10,35 @@ internal sealed class ParseOperation { private readonly List _tokens; private readonly CommandLineConfiguration _configuration; + private readonly string? _rawInput; + private readonly SymbolResultTree _symbolResultTree; + private readonly CommandResult _rootCommandResult; + private int _index; + private Dictionary>? _directives; + private CommandResult _innermostCommandResult; + private bool _isHelpRequested; public ParseOperation( List tokens, - CommandLineConfiguration configuration) + CommandLineConfiguration configuration, + List? tokenizeErrors, + string? rawInput) { _tokens = tokens; _configuration = configuration; + _rawInput = rawInput; + _symbolResultTree = new(configuration.LocalizationResources, tokenizeErrors); + _innermostCommandResult = _rootCommandResult = new CommandResult( + _configuration.RootCommand, + CurrentToken, + _symbolResultTree); + + Advance(); } private Token CurrentToken => _tokens[_index]; - public CommandNode? RootCommandNode { get; private set; } - - public List? UnmatchedTokens { get; private set; } - private void Advance() => _index++; private bool More(out TokenType currentTokenType) @@ -34,38 +48,46 @@ private bool More(out TokenType currentTokenType) return result; } - public void Parse() - { - RootCommandNode = ParseRootCommand(); - } - - private CommandNode ParseRootCommand() + internal ParseResult Parse(Parser parser) { - var rootCommandNode = new CommandNode( - CurrentToken, - _configuration.RootCommand); + ParseDirectives(); - Advance(); + ParseCommandChildren(); - ParseDirectives(rootCommandNode); - - ParseCommandChildren(rootCommandNode); + if (!_isHelpRequested) + { + Validate(); + } - return rootCommandNode; + return new( + parser, + _rootCommandResult, + _innermostCommandResult, + _directives, + _tokens, + _symbolResultTree.UnmatchedTokens, + _symbolResultTree.Errors, + _rawInput); } - private void ParseSubcommand(CommandNode parentNode) + private void ParseSubcommand() { - var commandNode = new CommandNode(CurrentToken, (Command)CurrentToken.Symbol!); + Command command = (Command)CurrentToken.Symbol!; - Advance(); + _innermostCommandResult = new CommandResult( + command, + CurrentToken, + _symbolResultTree, + _innermostCommandResult); - ParseCommandChildren(commandNode); + _symbolResultTree.Add(command, _innermostCommandResult); + + Advance(); - parentNode.AddChildNode(commandNode); + ParseCommandChildren(); } - private void ParseCommandChildren(CommandNode parent) + private void ParseCommandChildren() { int currentArgumentCount = 0; int currentArgumentIndex = 0; @@ -74,15 +96,15 @@ private void ParseCommandChildren(CommandNode parent) { if (currentTokenType == TokenType.Command) { - ParseSubcommand(parent); + ParseSubcommand(); } else if (currentTokenType == TokenType.Option) { - ParseOption(parent); + ParseOption(); } else if (currentTokenType == TokenType.Argument) { - ParseCommandArguments(parent, ref currentArgumentCount, ref currentArgumentIndex); + ParseCommandArguments(ref currentArgumentCount, ref currentArgumentIndex); } else { @@ -92,13 +114,13 @@ private void ParseCommandChildren(CommandNode parent) } } - private void ParseCommandArguments(CommandNode commandNode, ref int currentArgumentCount, ref int currentArgumentIndex) + private void ParseCommandArguments(ref int currentArgumentCount, ref int currentArgumentIndex) { while (More(out TokenType currentTokenType) && currentTokenType == TokenType.Argument) { - while (commandNode.Command.HasArguments && currentArgumentIndex < commandNode.Command.Arguments.Count) + while (_innermostCommandResult.Command.HasArguments && currentArgumentIndex < _innermostCommandResult.Command.Arguments.Count) { - Argument argument = commandNode.Command.Arguments[currentArgumentIndex]; + Argument argument = _innermostCommandResult.Command.Arguments[currentArgumentIndex]; if (currentArgumentCount < argument.Arity.MaximumNumberOfValues) { @@ -108,12 +130,20 @@ private void ParseCommandArguments(CommandNode commandNode, ref int currentArgum CurrentToken.Symbol = argument; } - var argumentNode = new CommandArgumentNode( - CurrentToken, - argument, - commandNode); + if (!(_symbolResultTree.TryGetValue(argument, out var symbolResult) + && symbolResult is ArgumentResult argumentResult)) + { + argumentResult = + new ArgumentResult( + argument, + _symbolResultTree, + _innermostCommandResult); + + _symbolResultTree.Add(argument, argumentResult); + } - commandNode.AddChildNode(argumentNode); + argumentResult.AddToken(CurrentToken); + _innermostCommandResult.AddToken(CurrentToken); currentArgumentCount++; @@ -136,22 +166,39 @@ private void ParseCommandArguments(CommandNode commandNode, ref int currentArgum } } - private void ParseOption(CommandNode parent) + private void ParseOption() { - OptionNode optionNode = new( - CurrentToken, - (Option)CurrentToken.Symbol!); + Option option = (Option)CurrentToken.Symbol!; + OptionResult optionResult; - Advance(); + if (!_symbolResultTree.TryGetValue(option, out SymbolResult symbolResult)) + { + if (option.DisallowBinding && option is HelpOption) + { + _isHelpRequested = true; + } + + optionResult = new OptionResult( + option, + _symbolResultTree, + CurrentToken, + _innermostCommandResult); - ParseOptionArguments(optionNode); + _symbolResultTree.Add(option, optionResult); + } + else + { + optionResult = (OptionResult)symbolResult; + } - parent.AddChildNode(optionNode); + Advance(); + + ParseOptionArguments(optionResult); } - private void ParseOptionArguments(OptionNode optionNode) + private void ParseOptionArguments(OptionResult optionResult) { - var argument = optionNode.Option.Argument; + var argument = optionResult.Option.Argument; var contiguousTokens = 0; int argumentCount = 0; @@ -162,24 +209,32 @@ private void ParseOptionArguments(OptionNode optionNode) { if (contiguousTokens > 0) { - return; + break; } if (argument.Arity.MaximumNumberOfValues == 0) { - return; + break; } } else if (argument.ValueType == typeof(bool) && !bool.TryParse(CurrentToken.Value, out _)) { - return; + break; } - optionNode.AddChildNode( - new OptionArgumentNode( - CurrentToken, - argument, - optionNode)); + if (!(_symbolResultTree.TryGetValue(argument, out SymbolResult? symbolResult) + && symbolResult is ArgumentResult argumentResult)) + { + argumentResult = new ArgumentResult( + argument, + _symbolResultTree, + optionResult); + + _symbolResultTree.Add(argument, argumentResult); + } + + argumentResult.AddToken(CurrentToken); + optionResult.AddToken(CurrentToken); argumentCount++; @@ -187,21 +242,27 @@ private void ParseOptionArguments(OptionNode optionNode) Advance(); - if (!optionNode.Option.AllowMultipleArgumentsPerToken) + if (!optionResult.Option.AllowMultipleArgumentsPerToken) { return; } } + + if (argumentCount == 0) + { + ArgumentResult argumentResult = new(optionResult.Option.Argument, _symbolResultTree, optionResult); + _symbolResultTree.Add(optionResult.Option.Argument, argumentResult); + } } - private void ParseDirectives(CommandNode rootCommandNode) + private void ParseDirectives() { while (More(out TokenType currentTokenType) && currentTokenType == TokenType.Directive) { - ParseDirective(rootCommandNode); // kept in separate method to avoid JIT + ParseDirective(); // kept in separate method to avoid JIT } - void ParseDirective(CommandNode parent) + void ParseDirective() { var token = CurrentToken; ReadOnlySpan withoutBrackets = token.Value.AsSpan(1, token.Value.Length - 2); @@ -213,9 +274,17 @@ void ParseDirective(CommandNode parent) ? withoutBrackets.Slice(indexOfColon + 1).ToString() : null; - var directiveNode = new DirectiveNode(token, key, value); + if (_directives is null || !_directives.TryGetValue(key, out var values)) + { + values = new List(); - parent.AddChildNode(directiveNode); + (_directives ??= new()).Add(key, values); + } + + if (value is not null) + { + ((List)values).Add(value); + } Advance(); } @@ -228,7 +297,22 @@ private void AddCurrentTokenToUnmatched() return; } - (UnmatchedTokens ??= new()).Add(CurrentToken); + _symbolResultTree.AddUnmatchedToken(CurrentToken); + } + + private void Validate() + { + // Only the inner most command goes through complete validation, + // for other commands only a subset of options is checked. + _innermostCommandResult.Validate(completeValidation: true); + + CommandResult? currentResult = _innermostCommandResult.Parent as CommandResult; + while (currentResult is not null) + { + currentResult.Validate(completeValidation: false); + + currentResult = currentResult.Parent as CommandResult; + } } } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/ParseResultVisitor.cs b/src/System.CommandLine/Parsing/ParseResultVisitor.cs deleted file mode 100644 index ed94ed50a6..0000000000 --- a/src/System.CommandLine/Parsing/ParseResultVisitor.cs +++ /dev/null @@ -1,603 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Generic; -using System.CommandLine.Binding; -using System.CommandLine.Completions; -using System.CommandLine.Help; -using System.Linq; - -namespace System.CommandLine.Parsing -{ - internal sealed class ParseResultVisitor - { - private readonly Parser _parser; - private readonly List _tokens; - private readonly string? _rawInput; - private readonly SymbolResultTree _symbolResultTree; - private readonly CommandResult _rootCommandResult; - - private Dictionary>? _directives; - private List? _unmatchedTokens; - private List? _errors; - private List? _argumentResults; - private CommandResult _innermostCommandResult; - private bool _isHelpRequested; - - internal ParseResultVisitor( - Parser parser, - List tokens, - List? tokenizeErrors, - List? unmatchedTokens, - string? rawInput, - CommandNode rootCommandNode) - { - _parser = parser; - _tokens = tokens; - _unmatchedTokens = unmatchedTokens; - _rawInput = rawInput; - _symbolResultTree = new(_parser.Configuration.LocalizationResources); - _innermostCommandResult = _rootCommandResult = new CommandResult( - rootCommandNode.Command, - rootCommandNode.Token, - _symbolResultTree); - - if (tokenizeErrors is not null) - { - _errors = new List(tokenizeErrors.Count); - - for (var i = 0; i < tokenizeErrors.Count; i++) - { - var error = tokenizeErrors[i]; - _errors.Add(new ParseError(error)); - } - } - } - - internal void Visit(CommandNode rootCommandNode) - { - VisitChildren(rootCommandNode); - - Stop(); - } - - private void VisitInternal(SyntaxNode node) - { - switch (node) - { - case DirectiveNode directiveNode: - VisitDirectiveNode(directiveNode); - - break; - - case CommandNode commandNode: - VisitCommandNode(commandNode); - - VisitChildren(commandNode); - - break; - - case OptionNode optionNode: - VisitOptionNode(optionNode); - - VisitChildren(optionNode); - - break; - - case CommandArgumentNode commandArgumentNode: - VisitCommandArgumentNode(commandArgumentNode); - - break; - - case OptionArgumentNode optionArgumentNode: - VisitOptionArgumentNode(optionArgumentNode); - - break; - } - } - - private void AddToResult(ArgumentResult result) - { - if (_symbolResultTree.TryAdd(result.Argument, result)) - { - (_argumentResults ??= new()).Add(result); - } - } - - private void VisitCommandNode(CommandNode commandNode) - { - var commandResult = new CommandResult( - commandNode.Command, - commandNode.Token, - _symbolResultTree, - _innermostCommandResult); - - _symbolResultTree.Add(commandNode.Command, commandResult); - - _innermostCommandResult = commandResult; - } - - private void VisitCommandArgumentNode(CommandArgumentNode argumentNode) - { - if (!(_symbolResultTree.TryGetValue(argumentNode.Argument, out var symbolResult) - && symbolResult is ArgumentResult argumentResult)) - { - argumentResult = - new ArgumentResult( - argumentNode.Argument, - _symbolResultTree, - _innermostCommandResult); - - AddToResult(argumentResult); - } - - argumentResult.AddToken(argumentNode.Token); - _innermostCommandResult.AddToken(argumentNode.Token); - } - - private void VisitOptionNode(OptionNode optionNode) - { - if (!_symbolResultTree.ContainsKey(optionNode.Option)) - { - if (optionNode.Option is HelpOption) - { - _isHelpRequested = true; - } - - var optionResult = new OptionResult( - optionNode.Option, - _symbolResultTree, - optionNode.Token, - _innermostCommandResult); - - _symbolResultTree.Add(optionNode.Option, optionResult); - - if (optionNode.Children is null) // no Arguments - { - if (optionResult.Option.Argument.HasCustomParser) - { - ArgumentResult argumentResult = new (optionResult.Option.Argument, _symbolResultTree, optionResult); - _symbolResultTree.Add(optionResult.Option.Argument, argumentResult); - } - } - } - } - - private void VisitOptionArgumentNode( - OptionArgumentNode argumentNode) - { - OptionResult optionResult = (OptionResult)_symbolResultTree[argumentNode.ParentOptionNode.Option]; - - var argument = argumentNode.Argument; - - if (!(_symbolResultTree.TryGetValue(argument, out SymbolResult? symbolResult) - && symbolResult is ArgumentResult argumentResult)) - { - argumentResult = - new ArgumentResult( - argumentNode.Argument, - _symbolResultTree, - optionResult); - _symbolResultTree.TryAdd(argument, argumentResult); - } - - argumentResult.AddToken(argumentNode.Token); - optionResult.AddToken(argumentNode.Token); - } - - private void VisitDirectiveNode(DirectiveNode directiveNode) - { - if (_directives is null || !_directives.TryGetValue(directiveNode.Name, out var values)) - { - values = new List(); - - (_directives ??= new()).Add(directiveNode.Name, values); - } - - if (directiveNode.Value is not null) - { - ((List)values).Add(directiveNode.Value); - } - } - - private void VisitChildren(NonterminalSyntaxNode parentNode) - { - if (parentNode.Children is not null) - { - for (var i = 0; i < parentNode.Children.Count; i++) - { - VisitInternal(parentNode.Children[i]); - } - } - } - - private void Stop() - { - if (_isHelpRequested) - { - return; - } - - ValidateCommandHandler(); - - PopulateDefaultValues(); - - ValidateCommandResult(); - - foreach (var symbolPair in _symbolResultTree) - { - if (symbolPair.Value is OptionResult optionResult) - { - ValidateAndConvertOptionResult(optionResult); - } - } - - if (_argumentResults is not null) - { - ValidateAndConvertArgumentResults(_innermostCommandResult.Command.Arguments, _argumentResults); - } - } - - private void ValidateAndConvertArgumentResults(IList arguments, List argumentResults) - { - int commandArgumentResultCount = argumentResults.Count; - - for (var i = 0; i < arguments.Count; i++) - { - if (i > 0 && argumentResults.Count > i) - { - var previousArgumentResult = argumentResults[i - 1]; - - var passedOnTokensCount = previousArgumentResult.PassedOnTokens?.Count; - - if (passedOnTokensCount > 0) - { - ShiftPassedOnTokensToNextResult(previousArgumentResult, argumentResults[i], passedOnTokensCount); - } - } - - // If this is the current last result but there are more arguments, see if we can shift tokens to the next argument - if (commandArgumentResultCount == i) - { - var nextArgument = arguments[i]; - var nextArgumentResult = new ArgumentResult( - nextArgument, - _symbolResultTree, - _innermostCommandResult); - - var previousArgumentResult = argumentResults[i - 1]; - - var passedOnTokensCount = _innermostCommandResult.Tokens.Count; - - ShiftPassedOnTokensToNextResult(previousArgumentResult, nextArgumentResult, passedOnTokensCount); - - argumentResults.Add(nextArgumentResult); - - if (previousArgumentResult.Parent is CommandResult) - { - AddToResult(nextArgumentResult); - } - - _symbolResultTree.TryAdd(nextArgumentResult.Argument, nextArgumentResult); - } - - if (commandArgumentResultCount >= argumentResults.Count) - { - var argumentResult = argumentResults[i]; - - ValidateAndConvertArgumentResult(argumentResult); - - if (argumentResult.PassedOnTokens is { } && - i == arguments.Count - 1) - { - _unmatchedTokens ??= new List(); - _unmatchedTokens.AddRange(argumentResult.PassedOnTokens); - } - } - } - - if (argumentResults.Count > arguments.Count) - { - for (var i = arguments.Count; i < argumentResults.Count - 1; i++) - { - var result = argumentResults[i]; - - if (result.Parent is CommandResult) - { - ValidateAndConvertArgumentResult(result); - } - } - } - - void ShiftPassedOnTokensToNextResult( - ArgumentResult previous, - ArgumentResult next, - int? numberOfTokens) - { - for (var j = previous.Tokens.Count; j < numberOfTokens; j++) - { - if (next.IsArgumentLimitReached) - { - break; - } - - next.AddToken(_innermostCommandResult.Tokens[j]); - } - } - } - - private void ValidateCommandResult() - { - var command = _innermostCommandResult.Command; - - if (command.HasValidators && UseValidators(command, _innermostCommandResult)) - { - return; - } - - bool checkOnlyGlobalOptions = false; - Command? currentCommand = command; - while (currentCommand is not null) - { - if (currentCommand.HasOptions) - { - var options = currentCommand.Options; - for (var i = 0; i < options.Count; i++) - { - var option = options[i]; - if (option.IsRequired && (!checkOnlyGlobalOptions || (checkOnlyGlobalOptions && option.IsGlobal))) - { - if (_rootCommandResult.FindResultFor(option) is null) - { - AddErrorToResult( - _innermostCommandResult, - new ParseError( - _rootCommandResult.LocalizationResources.RequiredOptionWasNotProvided(option), - _innermostCommandResult)); - } - } - } - } - - currentCommand = currentCommand.FirstParent?.Symbol as Command; - checkOnlyGlobalOptions = true; - } - - if (command.HasArguments) - { - ValidateArguments(command.Arguments, _innermostCommandResult); - } - } - - private bool UseValidators(Command command, CommandResult innermostCommandResult) - { - for (var i = 0; i < command.Validators.Count; i++) - { - var validateSymbolResult = command.Validators[i]; - validateSymbolResult(innermostCommandResult); - - if (!string.IsNullOrWhiteSpace(innermostCommandResult.ErrorMessage)) - { - AddErrorToResult( - innermostCommandResult, - new ParseError(innermostCommandResult.ErrorMessage!, _innermostCommandResult)); - - return true; - } - } - return false; - } - - private void ValidateArguments(IList arguments, CommandResult innermostCommandResult) - { - for (var i = 0; i < arguments.Count; i++) - { - var symbol = arguments[i]; - - var arityFailure = ArgumentArity.Validate( - innermostCommandResult, - symbol, - symbol.Arity.MinimumNumberOfValues, - symbol.Arity.MaximumNumberOfValues); - - if (arityFailure is not null) - { - AddErrorToResult(innermostCommandResult, new ParseError(arityFailure.ErrorMessage!, innermostCommandResult)); - } - } - } - - private void ValidateCommandHandler() - { - if (_innermostCommandResult.Command is not { Handler: null } cmd) - { - return; - } - - if (!cmd.HasSubcommands) - { - return; - } - - (_errors ??= new()).Insert( - 0, - new ParseError( - _innermostCommandResult.LocalizationResources.RequiredCommandWasNotProvided(), - _innermostCommandResult)); - } - - private void ValidateAndConvertOptionResult(OptionResult optionResult) - { - var argument = optionResult.Option.Argument; - - var arityFailure = ArgumentArity.Validate( - optionResult, - argument, - argument.Arity.MinimumNumberOfValues, - argument.Arity.MaximumNumberOfValues); - - if (arityFailure is { }) - { - AddErrorToResult(optionResult, new ParseError(arityFailure.ErrorMessage!, optionResult)); - return; - } - - if (optionResult.Option.HasValidators) - { - for (var i = 0; i < optionResult.Option.Validators.Count; i++) - { - var validate = optionResult.Option.Validators[i]; - validate(optionResult); - - if (!string.IsNullOrWhiteSpace(optionResult.ErrorMessage)) - { - AddErrorToResult(optionResult, new ParseError(optionResult.ErrorMessage!, optionResult)); - - return; - } - } - } - - foreach (var pair in _symbolResultTree) - { - if (object.ReferenceEquals(pair.Value.Parent, optionResult)) - { - ValidateAndConvertArgumentResult((ArgumentResult)pair.Value); - } - } - } - - private void ValidateAndConvertArgumentResult(ArgumentResult argumentResult) - { - var argument = argumentResult.Argument; - - var parseError = argumentResult.CustomError(argument); - - if (parseError is { }) - { - AddErrorToResult(argumentResult, parseError); - return; - } - - var argumentConversionResult = argumentResult.GetArgumentConversionResult(); - - if (argumentConversionResult.Result >= ArgumentConversionResultType.Failed && - argumentConversionResult.Result != ArgumentConversionResultType.FailedArity) - { - if (argument.FirstParent?.Symbol is Option option) - { - var completions = option.GetCompletions(CompletionContext.Empty).ToArray(); - - if (completions.Length > 0) - { - argumentConversionResult = ArgumentConversionResult.Failure( - argumentConversionResult.Argument, - argumentConversionResult.ErrorMessage + " Did you mean one of the following?" + Environment.NewLine + string.Join(Environment.NewLine, completions.Select(c => c.Label)), - argumentConversionResult.Result); - } - } - - AddErrorToResult(argumentResult, new ParseError(argumentConversionResult.ErrorMessage!, argumentResult)); - } - } - - private void PopulateDefaultValues() - { - var commandResult = _innermostCommandResult; - - while (commandResult is not null) - { - if (commandResult.Command.HasOptions) - { - var options = commandResult.Command.Options; - for (var i = 0; i < options.Count; i++) - { - Option option = options[i]; - Handle(_rootCommandResult.FindResultFor(option), option); - } - } - - if (commandResult.Command.HasArguments) - { - var arguments = commandResult.Command.Arguments; - for (var i = 0; i < arguments.Count; i++) - { - Argument argument = arguments[i]; - Handle(_rootCommandResult.FindResultFor(argument), argument); - } - } - - commandResult = commandResult.Parent as CommandResult; - } - - void Handle(SymbolResult? symbolResult, Symbol symbol) - { - switch (symbolResult) - { - case OptionResult o: - - if (o.Option.Argument.ValueType == typeof(bool) - && !_symbolResultTree.ContainsKey(o.Option.Argument)) - { - _symbolResultTree.Add(o.Option.Argument, new ArgumentResult(o.Option.Argument, _symbolResultTree, o)); - } - - break; - - case null: - switch (symbol) - { - case Option option when option.Argument.HasDefaultValue: - - var optionResult = new OptionResult( - option, - _symbolResultTree, - null, - commandResult); - - if (_symbolResultTree.TryAdd(optionResult.Option, optionResult)) - { - ArgumentResult argumentResult = new (optionResult.Option.Argument, _symbolResultTree, optionResult); - _symbolResultTree.Add(optionResult.Option.Argument, argumentResult); - } - - break; - - case Argument { HasDefaultValue: true } argument: - - if (!_symbolResultTree.ContainsKey(argument)) - { - AddToResult(new ArgumentResult(argument, _symbolResultTree, commandResult)); - } - - break; - } - - break; - } - } - } - - private void AddErrorToResult(SymbolResult symbolResult, ParseError parseError) - { - symbolResult.ErrorMessage ??= parseError.Message; - - if (symbolResult.Parent is OptionResult optionResult) - { - optionResult.ErrorMessage ??= symbolResult.ErrorMessage; - } - - (_errors ??= new()).Add(parseError); - } - - public ParseResult GetResult() => - new(_parser, - _rootCommandResult, - _innermostCommandResult, - _directives, - _tokens, - _unmatchedTokens, - _errors, - _rawInput); - } -} diff --git a/src/System.CommandLine/Parsing/Parser.cs b/src/System.CommandLine/Parsing/Parser.cs index 711e9e3e97..61046b2f90 100644 --- a/src/System.CommandLine/Parsing/Parser.cs +++ b/src/System.CommandLine/Parsing/Parser.cs @@ -47,21 +47,11 @@ public ParseResult Parse( var operation = new ParseOperation( tokens, - Configuration); - - operation.Parse(); - - var visitor = new ParseResultVisitor( - this, - tokens, + Configuration, tokenizationErrors, - operation.UnmatchedTokens, - rawInput, - operation.RootCommandNode!); - - visitor.Visit(operation.RootCommandNode!); + rawInput); - return visitor.GetResult(); + return operation.Parse(this); } } } diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/SymbolResult.cs index 6ab20d8151..a7ae8d5202 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/SymbolResult.cs @@ -21,12 +21,6 @@ private protected SymbolResult(SymbolResultTree symbolResultTree, SymbolResult? Parent = parent; } - /// - /// An error message for this symbol result. - /// - /// Setting this value to a non-null during parsing will cause the parser to indicate an error for the user and prevent invocation of the command line. - public string? ErrorMessage { get; set; } - /// /// The parent symbol result in the parse tree. /// @@ -37,13 +31,6 @@ private protected SymbolResult(SymbolResultTree symbolResultTree, SymbolResult? /// public IReadOnlyList Tokens => _tokens is not null ? _tokens : Array.Empty(); - internal bool IsArgumentLimitReached => RemainingArgumentCapacity == 0; - - private protected virtual int RemainingArgumentCapacity => - MaximumArgumentCapacity - Tokens.Count; - - internal abstract int MaximumArgumentCapacity { get; } - /// /// Localization resources used to produce messages for this symbol result. /// @@ -51,6 +38,12 @@ private protected SymbolResult(SymbolResultTree symbolResultTree, SymbolResult? internal void AddToken(Token token) => (_tokens ??= new()).Add(token); + /// + /// Adds an error message for this symbol result to it's parse tree. + /// + /// Setting an error will cause the parser to indicate an error for the user and prevent invocation of the command line. + public virtual void AddError(string errorMessage) => SymbolResultTree.AddError(new ParseError(errorMessage, this)); + /// /// Finds a result for the specific argument anywhere in the parse tree, including parent and child symbol results. /// @@ -120,7 +113,7 @@ public T GetValue(Argument argument) return ArgumentConverter.GetDefaultValue(option.Argument.ValueType); } - internal virtual bool UseDefaultValueFor(Argument argument) => false; + internal virtual bool UseDefaultValueFor(ArgumentResult argumentResult) => false; /// public override string ToString() => $"{GetType().Name}: {this.Token()} {string.Join(" ", Tokens.Select(t => t.Value))}"; diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 07ba985c7e..d59935f020 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -7,14 +7,26 @@ namespace System.CommandLine.Parsing { internal sealed class SymbolResultTree : Dictionary { - private readonly LocalizationResources _localizationResources; + internal readonly LocalizationResources LocalizationResources; + internal List? Errors; + internal List? UnmatchedTokens; - internal SymbolResultTree(LocalizationResources localizationResources) + internal SymbolResultTree(LocalizationResources localizationResources, List? tokenizeErrors) { - _localizationResources = localizationResources; + LocalizationResources = localizationResources; + + if (tokenizeErrors is not null) + { + Errors = new List(tokenizeErrors.Count); + + for (var i = 0; i < tokenizeErrors.Count; i++) + { + Errors.Add(new ParseError(tokenizeErrors[i])); + } + } } - internal LocalizationResources LocalizationResources => _localizationResources; + internal int ErrorCount => Errors?.Count ?? 0; internal ArgumentResult? FindResultFor(Argument argument) => TryGetValue(argument, out SymbolResult? result) ? (ArgumentResult)result : default; @@ -38,5 +50,11 @@ internal IEnumerable GetChildren(SymbolResult parent) } } } + + internal void AddError(ParseError parseError) => (Errors ??= new()).Add(parseError); + + internal void InsertFirstError(ParseError parseError) => (Errors ??= new()).Insert(0, parseError); + + internal void AddUnmatchedToken(Token token) => (UnmatchedTokens ??= new()).Add(token); } } diff --git a/src/System.CommandLine/Parsing/SyntaxNode.cs b/src/System.CommandLine/Parsing/SyntaxNode.cs deleted file mode 100644 index 5ad049f415..0000000000 --- a/src/System.CommandLine/Parsing/SyntaxNode.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace System.CommandLine.Parsing -{ - internal abstract class SyntaxNode - { - protected SyntaxNode(Token token) - { - Token = token; - } - - public Token Token { get; } - - public override string ToString() => Token.Value; - } -} diff --git a/src/System.CommandLine/Properties/Resources.Designer.cs b/src/System.CommandLine/Properties/Resources.Designer.cs index ddb11c322f..c781eb9f85 100644 --- a/src/System.CommandLine/Properties/Resources.Designer.cs +++ b/src/System.CommandLine/Properties/Resources.Designer.cs @@ -78,6 +78,15 @@ internal static string ArgumentConversionCannotParseForCommand { } } + /// + /// Looks up a localized string similar to Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3}. + /// + internal static string ArgumentConversionCannotParseForCommand_Completions { + get { + return ResourceManager.GetString("ArgumentConversionCannotParseForCommand_Completions", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot parse argument '{0}' for option '{1}' as expected type '{2}'.. /// @@ -87,6 +96,15 @@ internal static string ArgumentConversionCannotParseForOption { } } + /// + /// Looks up a localized string similar to Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3}. + /// + internal static string ArgumentConversionCannotParseForOption_Completions { + get { + return ResourceManager.GetString("ArgumentConversionCannotParseForOption_Completions", resourceCulture); + } + } + /// /// Looks up a localized string similar to Command '{0}' expects no more than {1} arguments, but {2} were provided.. /// diff --git a/src/System.CommandLine/Properties/Resources.resx b/src/System.CommandLine/Properties/Resources.resx index f0b101ebba..a5e32aa8dc 100644 --- a/src/System.CommandLine/Properties/Resources.resx +++ b/src/System.CommandLine/Properties/Resources.resx @@ -234,4 +234,10 @@ Option '{0}' is required. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + \ No newline at end of file diff --git a/src/System.CommandLine/Properties/xlf/Resources.cs.xlf b/src/System.CommandLine/Properties/xlf/Resources.cs.xlf index 86c81955a1..ef2f1568d8 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.cs.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.cs.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.de.xlf b/src/System.CommandLine/Properties/xlf/Resources.de.xlf index e47d4bc1ee..6b69459ae5 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.de.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.de.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.es.xlf b/src/System.CommandLine/Properties/xlf/Resources.es.xlf index 2e85b00d2f..8a346c3ed8 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.es.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.es.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.fr.xlf b/src/System.CommandLine/Properties/xlf/Resources.fr.xlf index e8afe2a034..2d611ac68f 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.fr.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.fr.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.it.xlf b/src/System.CommandLine/Properties/xlf/Resources.it.xlf index 040135ce41..146e7b4814 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.it.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.it.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.ja.xlf b/src/System.CommandLine/Properties/xlf/Resources.ja.xlf index aa9c63dce5..0f23a46804 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.ja.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.ja.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.ko.xlf b/src/System.CommandLine/Properties/xlf/Resources.ko.xlf index f5a4950dca..60c9971927 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.ko.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.ko.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.pl.xlf b/src/System.CommandLine/Properties/xlf/Resources.pl.xlf index b5cf7fd3f0..4d74816ac1 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.pl.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.pl.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.pt-BR.xlf b/src/System.CommandLine/Properties/xlf/Resources.pt-BR.xlf index 8f7e7af0d2..c1d25a631a 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.pt-BR.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.pt-BR.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.ru.xlf b/src/System.CommandLine/Properties/xlf/Resources.ru.xlf index 6a8b8128f5..2525b84023 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.ru.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.ru.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.tr.xlf b/src/System.CommandLine/Properties/xlf/Resources.tr.xlf index c43d076b4d..3835a328f0 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.tr.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.tr.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.zh-Hans.xlf b/src/System.CommandLine/Properties/xlf/Resources.zh-Hans.xlf index ca955abebc..0b5a526be0 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.zh-Hans.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.zh-Hans.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Properties/xlf/Resources.zh-Hant.xlf b/src/System.CommandLine/Properties/xlf/Resources.zh-Hant.xlf index 3ceabfdb3d..e52a13023e 100644 --- a/src/System.CommandLine/Properties/xlf/Resources.zh-Hant.xlf +++ b/src/System.CommandLine/Properties/xlf/Resources.zh-Hant.xlf @@ -12,11 +12,21 @@ Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for command '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. + + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. Did you mean one of the following?{3} + + Command '{0}' expects no more than {1} arguments, but {2} were provided. Command '{0}' expects no more than {1} arguments, but {2} were provided. diff --git a/src/System.CommandLine/Validate.cs b/src/System.CommandLine/Validate.cs index 04a5206a16..0b339174ed 100644 --- a/src/System.CommandLine/Validate.cs +++ b/src/System.CommandLine/Validate.cs @@ -13,8 +13,7 @@ internal static void FileExists(ArgumentResult result) if (!File.Exists(token.Value)) { - result.ErrorMessage = result.LocalizationResources.FileDoesNotExist(token.Value); - return; + result.AddError(result.LocalizationResources.FileDoesNotExist(token.Value)); } } } @@ -27,8 +26,7 @@ internal static void DirectoryExists(ArgumentResult result) if (!Directory.Exists(token.Value)) { - result.ErrorMessage = result.LocalizationResources.DirectoryDoesNotExist(token.Value); - return; + result.AddError(result.LocalizationResources.DirectoryDoesNotExist(token.Value)); } } } @@ -41,8 +39,7 @@ internal static void FileOrDirectoryExists(ArgumentResult result) if (!Directory.Exists(token.Value) && !File.Exists(token.Value)) { - result.ErrorMessage = result.LocalizationResources.FileOrDirectoryDoesNotExist(token.Value); - return; + result.AddError(result.LocalizationResources.FileOrDirectoryDoesNotExist(token.Value)); } } }