From 6ac690760731f7333a6b5d440d5b809717ba35d6 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 17 Feb 2024 11:55:05 -0500 Subject: [PATCH 001/150] Removed code and added back what cleared errors in CliParser --- src/System.CommandLine/CliCommand.cs | 11 +++++++---- src/System.CommandLine/CliConfiguration.cs | 7 ++++++- src/System.CommandLine/CliSymbol.cs | 4 +++- src/System.CommandLine/ParseResult.cs | 6 ++++-- src/System.CommandLine/Parsing/CliToken.cs | 4 +++- src/System.CommandLine/Parsing/ParseOperation.cs | 15 +++++++++++++-- .../Parsing/StringExtensions.cs | 11 ++++++++--- src/System.CommandLine/System.CommandLine.csproj | 12 +++++++++++- 8 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index e101bf3948..7e8cbb3ead 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -3,8 +3,8 @@ using System.Collections; using System.Collections.Generic; -using System.CommandLine.Completions; -using System.CommandLine.Invocation; +//using System.CommandLine.Completions; +//using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.ComponentModel; using System.Diagnostics; @@ -14,6 +14,7 @@ namespace System.CommandLine { + /// /// Represents a specific action that the application performs. /// @@ -22,8 +23,9 @@ namespace System.CommandLine /// for simple applications that only have one action. For example, dotnet run /// uses run as the command. /// - public class CliCommand : CliSymbol, IEnumerable + public class CliCommand : CliSymbol //, IEnumerable { + /* internal AliasSet? _aliases; private ChildSymbolList? _arguments; private ChildSymbolList? _options; @@ -309,5 +311,6 @@ void AddCompletionsFor(CliSymbol identifier, AliasSet? aliases) internal bool EqualsNameOrAlias(string name) => Name.Equals(name, StringComparison.Ordinal) || (_aliases is not null && _aliases.Contains(name)); - } + */ + } } diff --git a/src/System.CommandLine/CliConfiguration.cs b/src/System.CommandLine/CliConfiguration.cs index dc02b4e512..cdbdb1dd75 100644 --- a/src/System.CommandLine/CliConfiguration.cs +++ b/src/System.CommandLine/CliConfiguration.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using System.Threading; using System.IO; -using System.CommandLine.Invocation; +//using System.CommandLine.Invocation; namespace System.CommandLine { @@ -16,6 +16,7 @@ namespace System.CommandLine /// public class CliConfiguration { + /* private TextWriter? _output, _error; /// @@ -221,5 +222,9 @@ static CliSymbol GetChild(int index, CliCommand command, out AliasSet? aliases) return command.Options[index - command.Subcommands.Count]; } } + */ + public CliConfiguration(CliCommand command) + { + } } } \ No newline at end of file diff --git a/src/System.CommandLine/CliSymbol.cs b/src/System.CommandLine/CliSymbol.cs index 35ccd1887e..a62532bd3e 100644 --- a/src/System.CommandLine/CliSymbol.cs +++ b/src/System.CommandLine/CliSymbol.cs @@ -2,7 +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.Completions; +//using System.CommandLine.Completions; using System.Diagnostics; namespace System.CommandLine @@ -12,6 +12,7 @@ namespace System.CommandLine /// public abstract class CliSymbol { + /* private protected CliSymbol(string name, bool allowWhitespace = false) { Name = ThrowIfEmptyOrWithWhitespaces(name, nameof(name), allowWhitespace); @@ -99,5 +100,6 @@ internal static string ThrowIfEmptyOrWithWhitespaces(string value, string paramN return value; } + */ } } \ No newline at end of file diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index c7683060a8..ad3fd82635 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.CommandLine.Completions; -using System.CommandLine.Invocation; +//using System.CommandLine.Completions; +//using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Linq; using System.Threading.Tasks; @@ -16,6 +16,7 @@ namespace System.CommandLine /// public sealed class ParseResult { + /* private readonly CommandResult _rootCommandResult; private readonly IReadOnlyList _unmatchedTokens; private CompletionContext? _completionContext; @@ -336,5 +337,6 @@ static bool WillAcceptAnArgument( return !optionResult.IsArgumentLimitReached; } } + */ } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/CliToken.cs b/src/System.CommandLine/Parsing/CliToken.cs index e268459468..390832521d 100644 --- a/src/System.CommandLine/Parsing/CliToken.cs +++ b/src/System.CommandLine/Parsing/CliToken.cs @@ -6,8 +6,9 @@ namespace System.CommandLine.Parsing /// /// A unit of significant text on the command line. /// - public sealed class CliToken : IEquatable + public sealed class CliToken //: IEquatable { + /* internal const int ImplicitPosition = -1; /// The string value of the token. @@ -75,5 +76,6 @@ internal CliToken(string? value, CliTokenType type, CliSymbol? symbol, int posit /// The second . /// if the objects are not equal. public static bool operator !=(CliToken? left, CliToken? right) => left is null ? right is not null : !left.Equals(right); + */ } } diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 7b26fa9b9a..2f272d4c39 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -2,13 +2,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.CommandLine.Help; -using System.CommandLine.Invocation; +//using System.CommandLine.Help; +//using System.CommandLine.Invocation; namespace System.CommandLine.Parsing { internal sealed class ParseOperation { + /* private readonly List _tokens; private readonly CliConfiguration _configuration; private readonly string? _rawInput; @@ -380,5 +381,15 @@ private void Validate() currentResult = currentResult.Parent as CommandResult; } } + */ + public ParseOperation(List tokens, CliConfiguration configuration, List? tokenizationErrors, string? rawInput) + { + throw new NotImplementedException(); + } + + internal ParseResult Parse() + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 169070c5f7..31e5f77b86 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -10,6 +10,7 @@ namespace System.CommandLine.Parsing { internal static class StringExtensions { + /* internal static bool ContainsCaseInsensitive( this string source, string value) => @@ -43,6 +44,8 @@ internal static (string? Prefix, string Alias) SplitPrefix(this string rawAlias) return (null, rawAlias); } + */ + // this method is not returning a Value Tuple or a dedicated type to avoid JITting internal static void Tokenize( this IReadOnlyList args, @@ -51,7 +54,8 @@ internal static void Tokenize( out List tokens, out List? errors) { - const int FirstArgIsNotRootCommand = -1; + throw new NotImplementedException(); + /* const int FirstArgIsNotRootCommand = -1; List? errorList = null; @@ -287,9 +291,9 @@ bool PreviousTokenIsAnOptionExpectingAnArgument(out CliOption? option) option = null; return false; - } + }*/ } - + /* private static bool FirstArgumentIsRootCommand(IReadOnlyList args, CliCommand rootCommand, bool inferRootCommand) { if (args.Count > 0) @@ -507,5 +511,6 @@ static void AddOptionTokens(Dictionary tokens, CliOption optio } } } + */ } } \ No newline at end of file diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 17a23bebad..17dbe23551 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -1,4 +1,4 @@ - + true @@ -9,6 +9,7 @@ latest Support for parsing command lines, supporting both POSIX and Windows conventions and shell-agnostic command line completions. true + False @@ -23,6 +24,15 @@ + + + + + + + + + From 1a9221ad5b5f7b56f43bad6c2ba9905430a52ced Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 17 Feb 2024 12:47:18 -0500 Subject: [PATCH 002/150] Added more classes for CliParser --- src/System.CommandLine/CliArgument.cs | 6 ++++-- src/System.CommandLine/CliDirective.cs | 6 ++++-- src/System.CommandLine/CliOption.cs | 6 ++++-- src/System.CommandLine/CliOption{T}.cs | 2 ++ src/System.CommandLine/CliRootCommand.cs | 2 ++ src/System.CommandLine/Parsing/CliToken.cs | 4 +--- src/System.CommandLine/System.CommandLine.csproj | 5 +++++ 7 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/System.CommandLine/CliArgument.cs b/src/System.CommandLine/CliArgument.cs index aa453bfd72..7cc5b3e017 100644 --- a/src/System.CommandLine/CliArgument.cs +++ b/src/System.CommandLine/CliArgument.cs @@ -2,9 +2,9 @@ // 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.Binding; using System.CommandLine.Parsing; -using System.CommandLine.Completions; +//using System.CommandLine.Completions; using System.Linq; namespace System.CommandLine @@ -14,6 +14,7 @@ namespace System.CommandLine /// public abstract class CliArgument : CliSymbol { + /* private ArgumentArity _arity; private TryConvertArgument? _convertArguments; private List>>? _completionSources = null; @@ -131,5 +132,6 @@ public override IEnumerable GetCompletions(CompletionContext con public override string ToString() => $"{nameof(CliArgument)}: {Name}"; internal bool IsBoolean() => ValueType == typeof(bool) || ValueType == typeof(bool?); + */ } } diff --git a/src/System.CommandLine/CliDirective.cs b/src/System.CommandLine/CliDirective.cs index cb7930f5fe..f8d1993c87 100644 --- a/src/System.CommandLine/CliDirective.cs +++ b/src/System.CommandLine/CliDirective.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.CommandLine.Completions; -using System.CommandLine.Invocation; +//using System.CommandLine.Completions; +//using System.CommandLine.Invocation; namespace System.CommandLine { @@ -18,6 +18,7 @@ namespace System.CommandLine /// public class CliDirective : CliSymbol { + /* /// /// Initializes a new instance of the Directive class. /// @@ -36,5 +37,6 @@ public CliDirective(string name) /// public override IEnumerable GetCompletions(CompletionContext context) => Array.Empty(); + */ } } diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index fd204a8be4..d2f48efb9d 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.CommandLine.Completions; -using System.CommandLine.Invocation; +//using System.CommandLine.Completions; +//using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Linq; @@ -14,6 +14,7 @@ namespace System.CommandLine /// public abstract class CliOption : CliSymbol { + /* internal AliasSet? _aliases; private List>? _validators; @@ -132,5 +133,6 @@ public override IEnumerable GetCompletions(CompletionContext con .OrderBy(item => item.SortText.IndexOfCaseInsensitive(context.WordToComplete)) .ThenBy(symbol => symbol.Label, StringComparer.OrdinalIgnoreCase); } + */ } } diff --git a/src/System.CommandLine/CliOption{T}.cs b/src/System.CommandLine/CliOption{T}.cs index 0a9e857578..576fbf0b90 100644 --- a/src/System.CommandLine/CliOption{T}.cs +++ b/src/System.CommandLine/CliOption{T}.cs @@ -9,6 +9,7 @@ namespace System.CommandLine /// The that the option's arguments are expected to be parsed as. public class CliOption : CliOption { + /* internal readonly CliArgument _argument; /// @@ -60,5 +61,6 @@ public Func? DefaultValueFactory /// /// A parse error will result, for example, if file path separators are found in the parsed value. public void AcceptLegalFileNamesOnly() => _argument.AcceptLegalFileNamesOnly(); + */ } } \ No newline at end of file diff --git a/src/System.CommandLine/CliRootCommand.cs b/src/System.CommandLine/CliRootCommand.cs index 7c150b2440..ac7caca140 100644 --- a/src/System.CommandLine/CliRootCommand.cs +++ b/src/System.CommandLine/CliRootCommand.cs @@ -19,6 +19,7 @@ namespace System.CommandLine /// public class CliRootCommand : CliCommand { + /* private static Assembly? _assembly; private static string? _executablePath; private static string? _executableName; @@ -76,5 +77,6 @@ private static string GetExecutableVersion() return assemblyVersionAttribute.InformationalVersion; } } + */ } } diff --git a/src/System.CommandLine/Parsing/CliToken.cs b/src/System.CommandLine/Parsing/CliToken.cs index 390832521d..e268459468 100644 --- a/src/System.CommandLine/Parsing/CliToken.cs +++ b/src/System.CommandLine/Parsing/CliToken.cs @@ -6,9 +6,8 @@ namespace System.CommandLine.Parsing /// /// A unit of significant text on the command line. /// - public sealed class CliToken //: IEquatable + public sealed class CliToken : IEquatable { - /* internal const int ImplicitPosition = -1; /// The string value of the token. @@ -76,6 +75,5 @@ internal CliToken(string? value, CliTokenType type, CliSymbol? symbol, int posit /// The second . /// if the objects are not equal. public static bool operator !=(CliToken? left, CliToken? right) => left is null ? right is not null : !left.Equals(right); - */ } } diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 17dbe23551..8e7760424c 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -24,8 +24,12 @@ + + + + @@ -33,6 +37,7 @@ + From 8079cca482eb9c4b7a39beb80c93f9e5b46fe99e Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 17 Feb 2024 13:09:22 -0500 Subject: [PATCH 003/150] System.CommandLine.Tests building (empty) --- .../System.CommandLine.Tests.csproj | 11 ++++++----- .../{Argument{T}.cs => CliArgument{T}.cs} | 2 ++ src/System.CommandLine/CliRootCommand.cs | 4 ++-- src/System.CommandLine/System.CommandLine.csproj | 2 ++ 4 files changed, 12 insertions(+), 7 deletions(-) rename src/System.CommandLine/{Argument{T}.cs => CliArgument{T}.cs} (99%) diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index e282684919..7d37308dd6 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -1,9 +1,10 @@ - + $(TargetFrameworkForNETSDK);net462 false $(DefaultExcludesInProjectFolder);TestApps\** + False @@ -12,10 +13,10 @@ - - - - + + + + diff --git a/src/System.CommandLine/Argument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs similarity index 99% rename from src/System.CommandLine/Argument{T}.cs rename to src/System.CommandLine/CliArgument{T}.cs index 8cf1981020..6357e4b2f2 100644 --- a/src/System.CommandLine/Argument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -11,6 +11,7 @@ namespace System.CommandLine /// public class CliArgument : CliArgument { + /* private Func? _customParser; /// @@ -197,5 +198,6 @@ public void AcceptLegalFileNamesOnly() return default; } + */ } } diff --git a/src/System.CommandLine/CliRootCommand.cs b/src/System.CommandLine/CliRootCommand.cs index ac7caca140..b61d40f3ba 100644 --- a/src/System.CommandLine/CliRootCommand.cs +++ b/src/System.CommandLine/CliRootCommand.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.CommandLine.Completions; -using System.CommandLine.Help; +//using System.CommandLine.Completions; +//using System.CommandLine.Help; using System.IO; using System.Reflection; diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 8e7760424c..c15e491bd6 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -25,11 +25,13 @@ + + From 6f9a910db79a3f227c55218955b3313c128b39a7 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 17 Feb 2024 13:38:52 -0500 Subject: [PATCH 004/150] Get Tokenize method compiling TODO: * aliases * directives * ResponseFileTokenReplacer Moved static executable info helpers from RootCommand to separate helper class and made internal for now. # Conflicts: # src/System.CommandLine/CliCommand.cs --- src/System.CommandLine/CliCommand.cs | 24 +++++---- src/System.CommandLine/CliConfiguration.cs | 8 +-- src/System.CommandLine/CliExecutable.cs | 51 ++++++++++++++++++ src/System.CommandLine/CliOption.cs | 8 +-- src/System.CommandLine/CliRootCommand.cs | 37 ------------- src/System.CommandLine/CliSymbol.cs | 3 +- src/System.CommandLine/Parsing/CliParser.cs | 3 +- .../Parsing/StringExtensions.cs | 54 +++++++++++++------ .../System.CommandLine.csproj | 2 + 9 files changed, 115 insertions(+), 75 deletions(-) create mode 100644 src/System.CommandLine/CliExecutable.cs diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index 7e8cbb3ead..f027a8d18c 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -23,13 +23,15 @@ namespace System.CommandLine /// for simple applications that only have one action. For example, dotnet run /// uses run as the command. /// - public class CliCommand : CliSymbol //, IEnumerable + public class CliCommand : CliSymbol, IEnumerable { - /* +/* internal AliasSet? _aliases; +*/ private ChildSymbolList? _arguments; private ChildSymbolList? _options; private ChildSymbolList? _subcommands; + /* private List>? _validators; /// @@ -39,7 +41,7 @@ public class CliCommand : CliSymbol //, IEnumerable /// The description of the command, shown in help. public CliCommand(string name, string? description = null) : base(name) => Description = description; - + */ /// /// Gets the child symbols. /// @@ -78,7 +80,7 @@ public IEnumerable Children public IList Subcommands => _subcommands ??= new(this); internal bool HasSubcommands => _subcommands is not null && _subcommands.Count > 0; - +/* /// /// Validators to the command. Validators can be used /// to create custom validation logic. @@ -188,12 +190,12 @@ public void SetAction(Func> action) /// if set to and an extra command or argument is provided, validation will fail. /// public bool TreatUnmatchedTokensAsErrors { get; set; } = true; - +*/ /// [DebuggerStepThrough] [EditorBrowsable(EditorBrowsableState.Never)] // hide from intellisense, it's public for C# collection initializer IEnumerator IEnumerable.GetEnumerator() => Children.GetEnumerator(); - +/* /// /// Parses an array strings using the command. /// @@ -308,9 +310,9 @@ void AddCompletionsFor(CliSymbol identifier, AliasSet? aliases) } } } - - internal bool EqualsNameOrAlias(string name) - => Name.Equals(name, StringComparison.Ordinal) || (_aliases is not null && _aliases.Contains(name)); - */ - } +*/ + internal bool EqualsNameOrAlias(string name) => Name.Equals(name, StringComparison.Ordinal); + // TODO: aliases + //|| (_aliases is not null && _aliases.Contains(name)); + } } diff --git a/src/System.CommandLine/CliConfiguration.cs b/src/System.CommandLine/CliConfiguration.cs index cdbdb1dd75..c8461b7e24 100644 --- a/src/System.CommandLine/CliConfiguration.cs +++ b/src/System.CommandLine/CliConfiguration.cs @@ -34,7 +34,7 @@ public CliConfiguration(CliCommand rootCommand) CliRootCommand root => root.Directives.Count > 0, _ => false }; - + */ /// /// Enables the parser to recognize and expand POSIX-style bundled options. /// @@ -56,7 +56,7 @@ public CliConfiguration(CliCommand rootCommand) /// /// public bool EnablePosixBundling { get; set; } = true; - + /* /// /// Enables a default exception handler to catch any unhandled exceptions thrown during invocation. Enabled by default. /// @@ -77,12 +77,12 @@ public CliConfiguration(CliCommand rootCommand) /// When enabled, any token prefixed with @ can be replaced with zero or more other tokens. This is mostly commonly used to expand tokens from response files and interpolate them into a command line prior to parsing. /// public TryReplaceToken? ResponseFileTokenReplacer { get; set; } = StringExtensions.TryReadResponseFile; - + */ /// /// Gets the root command. /// public CliCommand RootCommand { get; } - + /* /// /// The standard output. Used by Help and other facilities that write non-error information. /// By default it's set to . diff --git a/src/System.CommandLine/CliExecutable.cs b/src/System.CommandLine/CliExecutable.cs new file mode 100644 index 0000000000..96b9908626 --- /dev/null +++ b/src/System.CommandLine/CliExecutable.cs @@ -0,0 +1,51 @@ +// 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.IO; +using System.Reflection; + +namespace System.CommandLine; + +//TODO: cull unused member, consider making public again +/// +/// Static helpers for determining information about the CLI executable. +/// +internal static class CliExecutable +{ + private static Assembly? _assembly; + private static string? _executablePath; + private static string? _executableName; + private static string? _executableVersion; + + internal static Assembly GetAssembly() + => _assembly ??= (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()); + + /// + /// The name of the currently running executable. + /// + public static string ExecutableName + => _executableName ??= Path.GetFileNameWithoutExtension(ExecutablePath).Replace(" ", ""); + + /// + /// The path to the currently running executable. + /// + public static string ExecutablePath => _executablePath ??= Environment.GetCommandLineArgs()[0]; + + internal static string ExecutableVersion => _executableVersion ??= GetExecutableVersion(); + + private static string GetExecutableVersion() + { + var assembly = GetAssembly(); + + var assemblyVersionAttribute = assembly.GetCustomAttribute(); + + if (assemblyVersionAttribute is null) + { + return assembly.GetName().Version?.ToString() ?? ""; + } + else + { + return assemblyVersionAttribute.InformationalVersion; + } + } +} diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index d2f48efb9d..299c37a061 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -89,10 +89,10 @@ public ArgumentArity Arity /// /// public bool AllowMultipleArgumentsPerToken { get; set; } - - internal virtual bool Greedy - => Argument.Arity.MinimumNumberOfValues > 0 && Argument.ValueType != typeof(bool); - + */ + internal virtual bool Greedy => throw new NotImplementedException(); + // => Argument.Arity.MinimumNumberOfValues > 0 && Argument.ValueType != typeof(bool); + /* /// /// Indicates whether the option is required when its parent command is invoked. /// diff --git a/src/System.CommandLine/CliRootCommand.cs b/src/System.CommandLine/CliRootCommand.cs index b61d40f3ba..cc8861c1f5 100644 --- a/src/System.CommandLine/CliRootCommand.cs +++ b/src/System.CommandLine/CliRootCommand.cs @@ -20,11 +20,6 @@ namespace System.CommandLine public class CliRootCommand : CliCommand { /* - private static Assembly? _assembly; - private static string? _executablePath; - private static string? _executableName; - private static string? _executableVersion; - /// The description of the command, shown in help. public CliRootCommand(string description = "") : base(ExecutableName, description) { @@ -45,38 +40,6 @@ public CliRootCommand(string description = "") : base(ExecutableName, descriptio /// Adds a to the command. /// public void Add(CliDirective directive) => Directives.Add(directive); - - internal static Assembly GetAssembly() - => _assembly ??= (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()); - - /// - /// The name of the currently running executable. - /// - public static string ExecutableName - => _executableName ??= Path.GetFileNameWithoutExtension(ExecutablePath).Replace(" ", ""); - - /// - /// The path to the currently running executable. - /// - public static string ExecutablePath => _executablePath ??= Environment.GetCommandLineArgs()[0]; - - internal static string ExecutableVersion => _executableVersion ??= GetExecutableVersion(); - - private static string GetExecutableVersion() - { - var assembly = GetAssembly(); - - var assemblyVersionAttribute = assembly.GetCustomAttribute(); - - if (assemblyVersionAttribute is null) - { - return assembly.GetName().Version?.ToString() ?? ""; - } - else - { - return assemblyVersionAttribute.InformationalVersion; - } - } */ } } diff --git a/src/System.CommandLine/CliSymbol.cs b/src/System.CommandLine/CliSymbol.cs index a62532bd3e..f29523a030 100644 --- a/src/System.CommandLine/CliSymbol.cs +++ b/src/System.CommandLine/CliSymbol.cs @@ -22,7 +22,7 @@ private protected CliSymbol(string name, bool allowWhitespace = false) /// Gets or sets the description of the symbol. /// public string? Description { get; set; } - + */ /// /// Gets the name of the symbol. /// @@ -49,6 +49,7 @@ internal void AddParent(CliSymbol symbol) current.Next = new SymbolNode(symbol); } } + /* /// /// Gets or sets a value indicating whether the symbol is hidden. diff --git a/src/System.CommandLine/Parsing/CliParser.cs b/src/System.CommandLine/Parsing/CliParser.cs index d05b9f0552..955ccb84d8 100644 --- a/src/System.CommandLine/Parsing/CliParser.cs +++ b/src/System.CommandLine/Parsing/CliParser.cs @@ -149,8 +149,9 @@ private static ParseResult Parse( configuration ??= new CliConfiguration(command); arguments.Tokenize( - configuration, + configuration.RootCommand, inferRootCommand: rawInput is not null, + configuration.EnablePosixBundling, out List tokens, out List? tokenizationErrors); diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 31e5f77b86..5ea9263209 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -49,32 +49,33 @@ internal static (string? Prefix, string Alias) SplitPrefix(this string rawAlias) // this method is not returning a Value Tuple or a dedicated type to avoid JITting internal static void Tokenize( this IReadOnlyList args, - CliConfiguration configuration, + CliCommand rootCommand, bool inferRootCommand, + bool enablePosixBundling, out List tokens, out List? errors) { - throw new NotImplementedException(); - /* const int FirstArgIsNotRootCommand = -1; + const int FirstArgIsNotRootCommand = -1; List? errorList = null; - var currentCommand = configuration.RootCommand; + var currentCommand = rootCommand; var foundDoubleDash = false; + // TODO: Directives var foundEndOfDirectives = false; var tokenList = new List(args.Count); - var knownTokens = configuration.RootCommand.ValidTokens(); + var knownTokens = rootCommand.ValidTokens(); - int i = FirstArgumentIsRootCommand(args, configuration.RootCommand, inferRootCommand) + int i = FirstArgumentIsRootCommand(args, rootCommand, inferRootCommand) ? 0 : FirstArgIsNotRootCommand; for (; i < args.Count; i++) { var arg = i == FirstArgIsNotRootCommand - ? configuration.RootCommand.Name + ? rootCommand.Name : args[i]; if (foundDoubleDash) @@ -92,6 +93,8 @@ internal static void Tokenize( continue; } + // TODO: Directives + /* if (!foundEndOfDirectives) { if (arg.Length > 2 && @@ -124,7 +127,10 @@ internal static void Tokenize( foundEndOfDirectives = true; } } + /* + // TODO: ResponseFileTokenReplacer + /* if (configuration.ResponseFileTokenReplacer is { } replacer && arg.GetReplaceableTokenValue() is { } value) { @@ -147,6 +153,7 @@ internal static void Tokenize( continue; } } + */ if (knownTokens.TryGetValue(arg, out var token)) { @@ -166,7 +173,7 @@ internal static void Tokenize( CliCommand cmd = (CliCommand)token.Symbol!; if (cmd != currentCommand) { - if (cmd != configuration.RootCommand) + if (cmd != rootCommand) { knownTokens = cmd.ValidTokens(); // config contains Directives, they are allowed only for RootCommand } @@ -193,7 +200,7 @@ internal static void Tokenize( tokenList.Add(Argument(rest)); } } - else if (!configuration.EnablePosixBundling || + else if (!enablePosixBundling || !CanBeUnbundled(arg) || !TryUnbundle(arg.AsSpan(1), i)) { @@ -212,7 +219,8 @@ internal static void Tokenize( CliToken DoubleDash() => new("--", CliTokenType.DoubleDash, default, i); - CliToken Directive(string value, CliDirective? directive) => new(value, CliTokenType.Directive, directive, i); + // TODO: Directives + // CliToken Directive(string value, CliDirective? directive) => new(value, CliTokenType.Directive, directive, i); } tokens = tokenList; @@ -291,14 +299,14 @@ bool PreviousTokenIsAnOptionExpectingAnArgument(out CliOption? option) option = null; return false; - }*/ + } } - /* + private static bool FirstArgumentIsRootCommand(IReadOnlyList args, CliCommand rootCommand, bool inferRootCommand) { if (args.Count > 0) { - if (inferRootCommand && args[0] == CliRootCommand.ExecutablePath) + if (inferRootCommand && args[0] == CliExecutable.ExecutablePath) { return true; } @@ -320,12 +328,13 @@ private static bool FirstArgumentIsRootCommand(IReadOnlyList args, CliCo return false; } + /* private static string? GetReplaceableTokenValue(this string arg) => arg.Length > 1 && arg[0] == '@' ? arg.Substring(1) : null; - + */ internal static bool TrySplitIntoSubtokens( this string arg, out string first, @@ -349,7 +358,7 @@ internal static bool TrySplitIntoSubtokens( rest = null; return false; } - + /* internal static bool TryReadResponseFile( string filePath, out IReadOnlyList? newTokens, @@ -412,12 +421,14 @@ static IEnumerable SplitLine(string line) yield return word; } } - } + }*/ private static Dictionary ValidTokens(this CliCommand command) { Dictionary tokens = new(StringComparer.Ordinal); + // TODO: Directives + /* if (command is CliRootCommand { Directives: IList directives }) { for (int i = 0; i < directives.Count; i++) @@ -427,6 +438,7 @@ private static Dictionary ValidTokens(this CliCommand command) tokens[tokenString] = new CliToken(tokenString, CliTokenType.Directive, directive, CliToken.ImplicitPosition); } } + */ AddCommandTokens(tokens, command); @@ -463,10 +475,13 @@ private static Dictionary ValidTokens(this CliCommand command) for (var i = 0; i < parentCommand.Options.Count; i++) { CliOption option = parentCommand.Options[i]; + // TODO: recursive options + /* if (option.Recursive) { AddOptionTokens(tokens, option); } + */ } } @@ -483,6 +498,8 @@ static void AddCommandTokens(Dictionary tokens, CliCommand cmd { tokens.Add(cmd.Name, new CliToken(cmd.Name, CliTokenType.Command, cmd, CliToken.ImplicitPosition)); + //TODO: Aliases + /* if (cmd._aliases is not null) { foreach (string childAlias in cmd._aliases) @@ -490,6 +507,7 @@ static void AddCommandTokens(Dictionary tokens, CliCommand cmd tokens.Add(childAlias, new CliToken(childAlias, CliTokenType.Command, cmd, CliToken.ImplicitPosition)); } } + */ } static void AddOptionTokens(Dictionary tokens, CliOption option) @@ -499,6 +517,8 @@ static void AddOptionTokens(Dictionary tokens, CliOption optio tokens.Add(option.Name, new CliToken(option.Name, CliTokenType.Option, option, CliToken.ImplicitPosition)); } + //TODO: Aliases + /* if (option._aliases is not null) { foreach (string childAlias in option._aliases) @@ -509,8 +529,8 @@ static void AddOptionTokens(Dictionary tokens, CliOption optio } } } + */ } } - */ } } \ No newline at end of file diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index c15e491bd6..d96788beae 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -24,11 +24,13 @@ + + From 10e75a949b4ab327f393cf2294b0d47c7794cdae Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 17 Feb 2024 14:59:48 -0500 Subject: [PATCH 005/150] Convert tokenizer from string array extension methods to CliTokenizer class --- .../LocalizationResources.cs | 10 +++--- src/System.CommandLine/Parsing/CliParser.cs | 3 +- .../Parsing/StringExtensions.cs | 34 ++++++++++--------- .../System.CommandLine.csproj | 2 ++ 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/System.CommandLine/LocalizationResources.cs b/src/System.CommandLine/LocalizationResources.cs index 18f3c33d76..c668f331c5 100644 --- a/src/System.CommandLine/LocalizationResources.cs +++ b/src/System.CommandLine/LocalizationResources.cs @@ -13,6 +13,7 @@ namespace System.CommandLine /// internal static class LocalizationResources { +/* /// /// Interpolates values into a localized string similar to Command '{0}' expects a single argument but {1} were provided. /// @@ -86,7 +87,7 @@ internal static string UnrecognizedArgument(string unrecognizedArg, IReadOnlyCol /// internal static string UnrecognizedCommandOrArgument(string arg) => GetResourceString(Properties.Resources.UnrecognizedCommandOrArgument, arg); - +*/ /// /// Interpolates values into a localized string similar to Response file not found '{0}'. /// @@ -98,7 +99,7 @@ internal static string ResponseFileNotFound(string filePath) => /// internal static string ErrorReadingResponseFile(string filePath, IOException e) => GetResourceString(Properties.Resources.ErrorReadingResponseFile, filePath, e.Message); - +/* /// /// Interpolates values into a localized string similar to Show help and usage information. /// @@ -232,7 +233,7 @@ internal static string ArgumentConversionCannotParseForOption(string value, stri internal static 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. /// @@ -251,7 +252,8 @@ private static string GetResourceString(string resourceString, params object[] f } return resourceString; } - +/* private static string GetOptionName(OptionResult optionResult) => optionResult.IdentifierToken?.Value ?? optionResult.Option.Name; +*/ } } diff --git a/src/System.CommandLine/Parsing/CliParser.cs b/src/System.CommandLine/Parsing/CliParser.cs index 955ccb84d8..942390977f 100644 --- a/src/System.CommandLine/Parsing/CliParser.cs +++ b/src/System.CommandLine/Parsing/CliParser.cs @@ -148,7 +148,8 @@ private static ParseResult Parse( configuration ??= new CliConfiguration(command); - arguments.Tokenize( + CliTokenizer.Tokenize( + arguments, configuration.RootCommand, inferRootCommand: rawInput is not null, configuration.EnablePosixBundling, diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 5ea9263209..8e1d5296eb 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -24,8 +24,12 @@ internal static int IndexOfCaseInsensitive( .IndexOf(source, value, CompareOptions.OrdinalIgnoreCase); + */ + } - internal static (string? Prefix, string Alias) SplitPrefix(this string rawAlias) + internal static class CliTokenizer + { + internal static (string? Prefix, string Alias) SplitPrefix(string rawAlias) { if (rawAlias[0] == '/') { @@ -44,11 +48,9 @@ internal static (string? Prefix, string Alias) SplitPrefix(this string rawAlias) return (null, rawAlias); } - */ - // this method is not returning a Value Tuple or a dedicated type to avoid JITting internal static void Tokenize( - this IReadOnlyList args, + IReadOnlyList args, CliCommand rootCommand, bool inferRootCommand, bool enablePosixBundling, @@ -66,7 +68,7 @@ internal static void Tokenize( var tokenList = new List(args.Count); - var knownTokens = rootCommand.ValidTokens(); + var knownTokens = GetValidTokens(rootCommand); int i = FirstArgumentIsRootCommand(args, rootCommand, inferRootCommand) ? 0 @@ -175,7 +177,7 @@ internal static void Tokenize( { if (cmd != rootCommand) { - knownTokens = cmd.ValidTokens(); // config contains Directives, they are allowed only for RootCommand + knownTokens = GetValidTokens(cmd); // config contains Directives, they are allowed only for RootCommand } currentCommand = cmd; tokenList.Add(Command(arg, cmd)); @@ -189,7 +191,7 @@ internal static void Tokenize( } } } - else if (arg.TrySplitIntoSubtokens(out var first, out var rest) && + else if (TrySplitIntoSubtokens(arg, out var first, out var rest) && knownTokens.TryGetValue(first, out var subtoken) && subtoken.Type == CliTokenType.Option) { @@ -328,15 +330,14 @@ private static bool FirstArgumentIsRootCommand(IReadOnlyList args, CliCo return false; } - /* - private static string? GetReplaceableTokenValue(this string arg) => + private static string? GetReplaceableTokenValue(string arg) => arg.Length > 1 && arg[0] == '@' ? arg.Substring(1) : null; - */ - internal static bool TrySplitIntoSubtokens( - this string arg, + + private static bool TrySplitIntoSubtokens( + string arg, out string first, out string? rest) { @@ -358,7 +359,8 @@ internal static bool TrySplitIntoSubtokens( rest = null; return false; } - /* + + // TODO: rename to TryTokenizeResponseFile internal static bool TryReadResponseFile( string filePath, out IReadOnlyList? newTokens, @@ -392,7 +394,7 @@ static IEnumerable ExpandResponseFile(string filePath) foreach (var p in SplitLine(line)) { - if (p.GetReplaceableTokenValue() is { } path) + if (GetReplaceableTokenValue(p) is { } path) { foreach (var q in ExpandResponseFile(path)) { @@ -421,9 +423,9 @@ static IEnumerable SplitLine(string line) yield return word; } } - }*/ + } - private static Dictionary ValidTokens(this CliCommand command) + private static Dictionary GetValidTokens(CliCommand command) { Dictionary tokens = new(StringComparer.Ordinal); diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index d96788beae..c743493d49 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -35,12 +35,14 @@ + + From b4d35279d24a0b68d5036bc94a284f0c845a7bb1 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 17 Feb 2024 15:15:51 -0500 Subject: [PATCH 006/150] Add back aliases --- src/System.CommandLine/CliCommand.cs | 6 +----- src/System.CommandLine/CliOption.cs | 2 +- src/System.CommandLine/CliSymbol.cs | 3 +-- src/System.CommandLine/Parsing/StringExtensions.cs | 6 ------ src/System.CommandLine/System.CommandLine.csproj | 1 + 5 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index f027a8d18c..62ad4b223f 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -25,9 +25,7 @@ namespace System.CommandLine /// public class CliCommand : CliSymbol, IEnumerable { -/* internal AliasSet? _aliases; -*/ private ChildSymbolList? _arguments; private ChildSymbolList? _options; private ChildSymbolList? _subcommands; @@ -311,8 +309,6 @@ void AddCompletionsFor(CliSymbol identifier, AliasSet? aliases) } } */ - internal bool EqualsNameOrAlias(string name) => Name.Equals(name, StringComparison.Ordinal); - // TODO: aliases - //|| (_aliases is not null && _aliases.Contains(name)); + internal bool EqualsNameOrAlias(string name) => Name.Equals(name, StringComparison.Ordinal) || (_aliases is not null && _aliases.Contains(name)); } } diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index 299c37a061..28c7f15d7f 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -14,8 +14,8 @@ namespace System.CommandLine /// public abstract class CliOption : CliSymbol { - /* internal AliasSet? _aliases; +/* private List>? _validators; private protected CliOption(string name, string[] aliases) : base(name) diff --git a/src/System.CommandLine/CliSymbol.cs b/src/System.CommandLine/CliSymbol.cs index f29523a030..2acddafce7 100644 --- a/src/System.CommandLine/CliSymbol.cs +++ b/src/System.CommandLine/CliSymbol.cs @@ -76,7 +76,7 @@ public IEnumerable Parents /// Gets completions for the symbol. /// public abstract IEnumerable GetCompletions(CompletionContext context); - +*/ /// public override string ToString() => $"{GetType().Name}: {Name}"; @@ -101,6 +101,5 @@ internal static string ThrowIfEmptyOrWithWhitespaces(string value, string paramN return value; } - */ } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 8e1d5296eb..f4710a9385 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -500,8 +500,6 @@ static void AddCommandTokens(Dictionary tokens, CliCommand cmd { tokens.Add(cmd.Name, new CliToken(cmd.Name, CliTokenType.Command, cmd, CliToken.ImplicitPosition)); - //TODO: Aliases - /* if (cmd._aliases is not null) { foreach (string childAlias in cmd._aliases) @@ -509,7 +507,6 @@ static void AddCommandTokens(Dictionary tokens, CliCommand cmd tokens.Add(childAlias, new CliToken(childAlias, CliTokenType.Command, cmd, CliToken.ImplicitPosition)); } } - */ } static void AddOptionTokens(Dictionary tokens, CliOption option) @@ -519,8 +516,6 @@ static void AddOptionTokens(Dictionary tokens, CliOption optio tokens.Add(option.Name, new CliToken(option.Name, CliTokenType.Option, option, CliToken.ImplicitPosition)); } - //TODO: Aliases - /* if (option._aliases is not null) { foreach (string childAlias in option._aliases) @@ -531,7 +526,6 @@ static void AddOptionTokens(Dictionary tokens, CliOption optio } } } - */ } } } diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index c743493d49..1138ba1ce3 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -24,6 +24,7 @@ + From 4b6187499a5c8b2631fd71d1a2d71bb562de1fda Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 17 Feb 2024 18:21:28 -0500 Subject: [PATCH 007/150] Created Tokenizer test --- Directory.Build.props | 2 +- .../System.CommandLine.Tests.csproj | 5 +++-- src/System.CommandLine/CliArgument.cs | 2 ++ src/System.CommandLine/CliArgument{T}.cs | 4 ++++ src/System.CommandLine/CliCommand.cs | 14 +++++++++++--- src/System.CommandLine/CliDirective.cs | 2 +- src/System.CommandLine/CliOption.cs | 4 +++- src/System.CommandLine/CliOption{T}.cs | 12 +++++++----- src/System.CommandLine/CliRootCommand.cs | 7 +++++-- src/System.CommandLine/CliSymbol.cs | 2 +- src/System.CommandLine/System.CommandLine.csproj | 1 + 11 files changed, 39 insertions(+), 16 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 62f3090979..b619acaa4f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ $(NoWarn);NU5125;CS0618 MIT - 10.0 + 12.0 diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index 7d37308dd6..fece53366d 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -18,6 +18,8 @@ + + @@ -36,8 +38,7 @@ - + diff --git a/src/System.CommandLine/CliArgument.cs b/src/System.CommandLine/CliArgument.cs index 7cc5b3e017..e964f889a2 100644 --- a/src/System.CommandLine/CliArgument.cs +++ b/src/System.CommandLine/CliArgument.cs @@ -20,10 +20,12 @@ public abstract class CliArgument : CliSymbol private List>>? _completionSources = null; private List>? _validators = null; + */ private protected CliArgument(string name) : base(name, allowWhitespace: true) { } + /* /// /// Gets or sets the arity of the argument. /// diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index 6357e4b2f2..edf86ee1be 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -18,10 +18,14 @@ public class CliArgument : CliArgument /// Initializes a new instance of the Argument class. /// /// The name of the argument. It's not used for parsing, only when displaying Help or creating parse errors.> + /// + */ public CliArgument(string name) : base(name) { } + /* + /// /// The delegate to invoke to create the default value. /// diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index 62ad4b223f..092805c8e1 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -32,14 +32,18 @@ public class CliCommand : CliSymbol, IEnumerable /* private List>? _validators; + */ /// /// Initializes a new instance of the Command class. /// /// The name of the command. /// The description of the command, shown in help. - public CliCommand(string name, string? description = null) : base(name) - => Description = description; - */ + public CliCommand(string name)/*, string? description = null) */ + : base(name) + { + } + //=> Description = description; + /// /// Gets the child symbols. /// @@ -165,6 +169,8 @@ public void SetAction(Func> action) Action = new AnonymousAsynchronousCliAction(action); } + */ + /// /// Adds a to the command. /// @@ -183,6 +189,8 @@ public void SetAction(Func> action) /// The Command to add to the command. public void Add(CliCommand command) => Subcommands.Add(command); + /* + /// /// Gets or sets a value that indicates whether unmatched tokens should be treated as errors. For example, /// if set to and an extra command or argument is provided, validation will fail. diff --git a/src/System.CommandLine/CliDirective.cs b/src/System.CommandLine/CliDirective.cs index f8d1993c87..61bfdbdaaf 100644 --- a/src/System.CommandLine/CliDirective.cs +++ b/src/System.CommandLine/CliDirective.cs @@ -18,7 +18,6 @@ namespace System.CommandLine /// public class CliDirective : CliSymbol { - /* /// /// Initializes a new instance of the Directive class. /// @@ -28,6 +27,7 @@ public CliDirective(string name) { } + /* /// /// Gets or sets the for the Directive. The handler represents the action /// that will be performed when the Directive is invoked. diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index 28c7f15d7f..44d11e725a 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -18,6 +18,8 @@ public abstract class CliOption : CliSymbol /* private List>? _validators; + */ + private protected CliOption(string name, string[] aliases) : base(name) { if (aliases is { Length: > 0 }) @@ -26,11 +28,11 @@ private protected CliOption(string name, string[] aliases) : base(name) } } +/* /// /// Gets the argument for the option. /// internal abstract CliArgument Argument { get; } - /// /// Specifies if a default value is defined for the option. /// diff --git a/src/System.CommandLine/CliOption{T}.cs b/src/System.CommandLine/CliOption{T}.cs index 576fbf0b90..0621cebc62 100644 --- a/src/System.CommandLine/CliOption{T}.cs +++ b/src/System.CommandLine/CliOption{T}.cs @@ -11,24 +11,26 @@ public class CliOption : CliOption { /* internal readonly CliArgument _argument; - + */ /// /// Initializes a new instance of the class. /// /// The name of the option. It's used for parsing, displaying Help and creating parse errors.> /// Optional aliases. Used for parsing, suggestions and displayed in Help. public CliOption(string name, params string[] aliases) - : this(name, aliases, new CliArgument(name)) + /* : this(name, aliases, new CliArgument(name)) { } private protected CliOption(string name, string[] aliases, CliArgument argument) - : base(name, aliases) + */ : base(name, aliases) { - argument.AddParent(this); - _argument = argument; + //argument.AddParent(this); + //_argument = argument; } + /* + /// public Func? DefaultValueFactory { diff --git a/src/System.CommandLine/CliRootCommand.cs b/src/System.CommandLine/CliRootCommand.cs index cc8861c1f5..fa08130c84 100644 --- a/src/System.CommandLine/CliRootCommand.cs +++ b/src/System.CommandLine/CliRootCommand.cs @@ -19,17 +19,20 @@ namespace System.CommandLine /// public class CliRootCommand : CliCommand { - /* /// The description of the command, shown in help. - public CliRootCommand(string description = "") : base(ExecutableName, description) + public CliRootCommand(/*string description = "" */) + : base(CliExecutable.ExecutableName/*, description*/) { + /* Options.Add(new HelpOption()); Options.Add(new VersionOption()); Directives = new ChildSymbolList(this) { new SuggestDirective() }; + */ } + /* /// /// Represents all of the directives that are valid under the root command. diff --git a/src/System.CommandLine/CliSymbol.cs b/src/System.CommandLine/CliSymbol.cs index 2acddafce7..ecdf9fcd1b 100644 --- a/src/System.CommandLine/CliSymbol.cs +++ b/src/System.CommandLine/CliSymbol.cs @@ -12,11 +12,11 @@ namespace System.CommandLine /// public abstract class CliSymbol { - /* private protected CliSymbol(string name, bool allowWhitespace = false) { Name = ThrowIfEmptyOrWithWhitespaces(name, nameof(name), allowWhitespace); } + /* /// /// Gets or sets the description of the symbol. diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 1138ba1ce3..9d0de65a43 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -45,6 +45,7 @@ + From 851446aeaa0ee167bf51b9f9d7fa28dfc8243922 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 18 Feb 2024 10:57:11 -0500 Subject: [PATCH 008/150] Added tokenizer test --- .../TokenizerTests.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/System.CommandLine.Tests/TokenizerTests.cs diff --git a/src/System.CommandLine.Tests/TokenizerTests.cs b/src/System.CommandLine.Tests/TokenizerTests.cs new file mode 100644 index 0000000000..fe1a36870e --- /dev/null +++ b/src/System.CommandLine.Tests/TokenizerTests.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.CommandLine.Parsing; +using System.IO; +using FluentAssertions; +using FluentAssertions.Equivalency; +using System.Linq; +using FluentAssertions.Common; +using Xunit; +using Xunit.Abstractions; + + +namespace System.CommandLine.Tests +{ + public partial class TokenizerTests + { + + [Fact] + public void The_tokenizer_is_accessible() + { + var option = new CliOption("--hello"); + var command = new CliRootCommand { option }; + IReadOnlyList args = ["--hello", "world"]; + List tokens = null; + List errors = null; + CliTokenizer.Tokenize(args,command,false, true, out tokens, out errors); + + //result.GetResult(animalsOption) + // .Tokens + // .Select(t => t.Value) + // .Should() + // .BeEquivalentTo("cat", "dog"); + + tokens + .Skip(1) + .Select(t => t.Value) + .Should() + .BeEquivalentTo("--hello", "world"); + + errors.Should().BeNull(); + } + } +} From e69c34144576d5540e24906e6680750eb454d8d2 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 17 Feb 2024 21:33:49 -0500 Subject: [PATCH 009/150] Get ParseOperation compiling --- .../Binding/ArgumentConversionResult.cs | 8 ++- src/System.CommandLine/CliArgument.cs | 32 +++++++----- src/System.CommandLine/CliArgument{T}.cs | 19 +++---- src/System.CommandLine/CliCommand.cs | 1 + src/System.CommandLine/CliOption.cs | 21 ++++++-- src/System.CommandLine/CliOption{T}.cs | 19 ++++--- src/System.CommandLine/CliRootCommand.cs | 9 ++-- .../LocalizationResources.cs | 14 +++--- src/System.CommandLine/ParseResult.cs | 41 +++++++++++++--- .../Parsing/ArgumentResult.cs | 5 +- src/System.CommandLine/Parsing/CliParser.cs | 15 +++--- src/System.CommandLine/Parsing/CliToken.cs | 4 +- .../Parsing/CommandResult.cs | 17 +++++-- .../Parsing/OptionResult.cs | 7 ++- src/System.CommandLine/Parsing/ParseError.cs | 2 + .../Parsing/ParseOperation.cs | 49 +++++++++++-------- .../Parsing/SymbolResult.cs | 17 ++++--- .../Parsing/SymbolResultTree.cs | 20 ++++++-- .../System.CommandLine.csproj | 20 +++++++- 19 files changed, 217 insertions(+), 103 deletions(-) diff --git a/src/System.CommandLine/Binding/ArgumentConversionResult.cs b/src/System.CommandLine/Binding/ArgumentConversionResult.cs index 03bd2f85f0..6a22e6e98f 100644 --- a/src/System.CommandLine/Binding/ArgumentConversionResult.cs +++ b/src/System.CommandLine/Binding/ArgumentConversionResult.cs @@ -1,7 +1,7 @@ // 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.Completions; using System.CommandLine.Parsing; using System.Linq; @@ -48,6 +48,8 @@ private static string FormatErrorMessage( if (argumentResult.Parent is CommandResult commandResult) { string alias = commandResult.Command.Name; +// TODO: completion +/* CompletionItem[] completionItems = argumentResult.Argument.GetCompletions(CompletionContext.Empty).ToArray(); if (completionItems.Length > 0) @@ -56,6 +58,7 @@ private static string FormatErrorMessage( value, alias, expectedType, completionItems.Select(ci => ci.Label)); } else +*/ { return LocalizationResources.ArgumentConversionCannotParseForCommand(value, alias, expectedType); } @@ -63,6 +66,8 @@ private static string FormatErrorMessage( else if (argumentResult.Parent is OptionResult optionResult) { string alias = optionResult.Option.Name; +// TODO: completion +/* CompletionItem[] completionItems = optionResult.Option.GetCompletions(CompletionContext.Empty).ToArray(); if (completionItems.Length > 0) @@ -71,6 +76,7 @@ private static string FormatErrorMessage( value, alias, expectedType, completionItems.Select(ci => ci.Label)); } else +*/ { return LocalizationResources.ArgumentConversionCannotParseForOption(value, alias, expectedType); } diff --git a/src/System.CommandLine/CliArgument.cs b/src/System.CommandLine/CliArgument.cs index e964f889a2..a16365b1b5 100644 --- a/src/System.CommandLine/CliArgument.cs +++ b/src/System.CommandLine/CliArgument.cs @@ -2,7 +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.Binding; +using System.CommandLine.Binding; using System.CommandLine.Parsing; //using System.CommandLine.Completions; using System.Linq; @@ -14,18 +14,17 @@ namespace System.CommandLine /// public abstract class CliArgument : CliSymbol { - /* private ArgumentArity _arity; +// TODO: custom parser, completion, validators +/* private TryConvertArgument? _convertArguments; private List>>? _completionSources = null; private List>? _validators = null; - - */ +*/ private protected CliArgument(string name) : base(name, allowWhitespace: true) { } - /* /// /// Gets or sets the arity of the argument. /// @@ -42,18 +41,24 @@ public ArgumentArity Arity } set => _arity = value; } - +// TODO: help, completion +/* /// /// The name used in help output to describe the argument. /// public string? HelpName { get; set; } - - internal TryConvertArgument? ConvertArguments +*/ + internal TryConvertArgument? ConvertArguments => ArgumentConverter.GetConverter(this); +// TODO: custom parsers +/* { get => _convertArguments ??= ArgumentConverter.GetConverter(this); set => _convertArguments = value; } +*/ +// TODO: completion; +/* /// /// Gets the list of completion sources for the argument. /// @@ -91,12 +96,13 @@ public List>> CompletionSour return _completionSources; } } - +*/ /// /// Gets or sets the that the argument's parsed tokens will be converted to. /// public abstract Type ValueType { get; } +/* TODO: validators /// /// Provides a list of argument validators. Validators can be used /// to provide custom errors based on user input. @@ -104,7 +110,7 @@ public List>> CompletionSour public List> Validators => _validators ??= new (); internal bool HasValidators => (_validators?.Count ?? 0) > 0; - +*/ /// /// Gets the default value for the argument. /// @@ -120,7 +126,8 @@ public List>> CompletionSour /// Specifies if a default value is defined for the argument. /// public abstract bool HasDefaultValue { get; } - +// TODO: completion +/* /// public override IEnumerable GetCompletions(CompletionContext context) { @@ -129,11 +136,10 @@ public override IEnumerable GetCompletions(CompletionContext con .Distinct() .OrderBy(c => c.SortText, StringComparer.OrdinalIgnoreCase); } - +*/ /// public override string ToString() => $"{nameof(CliArgument)}: {Name}"; internal bool IsBoolean() => ValueType == typeof(bool) || ValueType == typeof(bool?); - */ } } diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index edf86ee1be..b8d4adb0db 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -11,21 +11,19 @@ namespace System.CommandLine /// public class CliArgument : CliArgument { - /* +// TODO: custom parser +/* private Func? _customParser; - +*/ /// /// Initializes a new instance of the Argument class. /// /// The name of the argument. It's not used for parsing, only when displaying Help or creating parse errors.> - /// - */ + /// public CliArgument(string name) : base(name) { } - /* - /// /// The delegate to invoke to create the default value. /// @@ -36,6 +34,8 @@ public CliArgument(string name) : base(name) /// public Func? DefaultValueFactory { get; set; } +// TODO: custom parsers +/* /// /// A custom argument parser. /// @@ -72,7 +72,7 @@ public CliArgument(string name) : base(name) } } } - +*/ /// public override Type ValueType => typeof(T); @@ -88,7 +88,8 @@ public CliArgument(string name) : base(name) return DefaultValueFactory.Invoke(argumentResult); } - +// TODO: completion, validators +/* /// /// Configures the argument to accept only the specified values, and to suggest them as command line completions. /// @@ -167,6 +168,7 @@ public void AcceptLegalFileNamesOnly() } }); } +*/ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "https://github.com/dotnet/command-line-api/issues/1638")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2091", Justification = "https://github.com/dotnet/command-line-api/issues/1638")] @@ -202,6 +204,5 @@ public void AcceptLegalFileNamesOnly() return default; } - */ } } diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index 092805c8e1..4d7fb61faf 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -25,6 +25,7 @@ namespace System.CommandLine /// public class CliCommand : CliSymbol, IEnumerable { + // TODO: don't expose field internal AliasSet? _aliases; private ChildSymbolList? _arguments; private ChildSymbolList? _options; diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index 44d11e725a..da146d256e 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -14,6 +14,7 @@ namespace System.CommandLine /// public abstract class CliOption : CliSymbol { + // TODO: don't expose field internal AliasSet? _aliases; /* private List>? _validators; @@ -28,16 +29,18 @@ private protected CliOption(string name, string[] aliases) : base(name) } } -/* /// /// Gets the argument for the option. /// internal abstract CliArgument Argument { get; } + /// /// Specifies if a default value is defined for the option. /// public bool HasDefaultValue => Argument.HasDefaultValue; +// TODO: help +/* /// /// Gets or sets the name of the Option when displayed in help. /// @@ -50,6 +53,7 @@ public string? HelpName get => Argument.HelpName; set => Argument.HelpName = value; } +*/ /// /// Gets or sets the arity of the option. @@ -60,6 +64,8 @@ public ArgumentArity Arity set => Argument.Arity = value; } +// TODO: recursive options, validators, completion +/* /// /// When set to true, this option will be applied to its immediate parent command or commands and recursively to their subcommands. /// @@ -76,7 +82,9 @@ public ArgumentArity Arity /// Gets the list of completion sources for the option. /// public List>> CompletionSources => Argument.CompletionSources; +*/ +// TODO: what does this even mean? /// /// Gets a value that indicates whether multiple argument tokens are allowed for each option identifier token. /// @@ -91,10 +99,11 @@ public ArgumentArity Arity /// /// public bool AllowMultipleArgumentsPerToken { get; set; } - */ - internal virtual bool Greedy => throw new NotImplementedException(); - // => Argument.Arity.MinimumNumberOfValues > 0 && Argument.ValueType != typeof(bool); - /* + +// TODO: rename to IsGreedy + internal virtual bool Greedy => Argument.Arity.MinimumNumberOfValues > 0 && Argument.ValueType != typeof(bool); + +// TODO: rename to IsRequired /// /// Indicates whether the option is required when its parent command is invoked. /// @@ -107,6 +116,8 @@ public ArgumentArity Arity /// The collection does not contain the of the Option. public ICollection Aliases => _aliases ??= new(); +// TODO: invocation, completion +/* /// /// Gets or sets the for the Option. The handler represents the action /// that will be performed when the Option is invoked. diff --git a/src/System.CommandLine/CliOption{T}.cs b/src/System.CommandLine/CliOption{T}.cs index 0621cebc62..fff51402d1 100644 --- a/src/System.CommandLine/CliOption{T}.cs +++ b/src/System.CommandLine/CliOption{T}.cs @@ -9,28 +9,26 @@ namespace System.CommandLine /// The that the option's arguments are expected to be parsed as. public class CliOption : CliOption { - /* +// TODO: do not expose private fields internal readonly CliArgument _argument; - */ + /// /// Initializes a new instance of the class. /// /// The name of the option. It's used for parsing, displaying Help and creating parse errors.> /// Optional aliases. Used for parsing, suggestions and displayed in Help. public CliOption(string name, params string[] aliases) - /* : this(name, aliases, new CliArgument(name)) + : this(name, aliases, new CliArgument(name)) { } private protected CliOption(string name, string[] aliases, CliArgument argument) - */ : base(name, aliases) + : base(name, aliases) { - //argument.AddParent(this); - //_argument = argument; + argument.AddParent(this); + _argument = argument; } - /* - /// public Func? DefaultValueFactory { @@ -38,15 +36,20 @@ public Func? DefaultValueFactory set => _argument.DefaultValueFactory = value; } +// TODO: custom parser +/* /// public Func? CustomParser { get => _argument.CustomParser; set => _argument.CustomParser = value; } +*/ internal sealed override CliArgument Argument => _argument; +// TODO: completion, validator +/* /// /// Configures the option to accept only the specified values, and to suggest them as command line completions. /// diff --git a/src/System.CommandLine/CliRootCommand.cs b/src/System.CommandLine/CliRootCommand.cs index fa08130c84..352ef5bedb 100644 --- a/src/System.CommandLine/CliRootCommand.cs +++ b/src/System.CommandLine/CliRootCommand.cs @@ -25,15 +25,16 @@ public CliRootCommand(/*string description = "" */) { /* Options.Add(new HelpOption()); - Options.Add(new VersionOption()); + Options.Add(new VersionOption()); Directives = new ChildSymbolList(this) { new SuggestDirective() }; */ } - /* - + +// TODO: directives +/* /// /// Represents all of the directives that are valid under the root command. /// @@ -43,6 +44,6 @@ public CliRootCommand(/*string description = "" */) /// Adds a to the command. /// public void Add(CliDirective directive) => Directives.Add(directive); - */ +*/ } } diff --git a/src/System.CommandLine/LocalizationResources.cs b/src/System.CommandLine/LocalizationResources.cs index c668f331c5..fc376d7cf4 100644 --- a/src/System.CommandLine/LocalizationResources.cs +++ b/src/System.CommandLine/LocalizationResources.cs @@ -13,13 +13,12 @@ namespace System.CommandLine /// internal static class LocalizationResources { -/* /// /// Interpolates values into a localized string similar to Command '{0}' expects a single argument but {1} were provided. /// internal static string ExpectsOneArgument(OptionResult optionResult) => GetResourceString(Properties.Resources.OptionExpectsOneArgument, GetOptionName(optionResult), optionResult.Tokens.Count); - +/* /// /// Interpolates values into a localized string similar to Directory does not exist: {0}. /// @@ -49,7 +48,7 @@ internal static string InvalidCharactersInPath(char invalidChar) => /// internal static string InvalidCharactersInFileName(char invalidChar) => GetResourceString(Properties.Resources.InvalidCharactersInFileName, invalidChar); - +*/ /// /// Interpolates values into a localized string similar to Required argument missing for command: {0}. /// @@ -87,7 +86,7 @@ internal static string UnrecognizedArgument(string unrecognizedArg, IReadOnlyCol /// internal static string UnrecognizedCommandOrArgument(string arg) => GetResourceString(Properties.Resources.UnrecognizedCommandOrArgument, arg); -*/ + /// /// Interpolates values into a localized string similar to Response file not found '{0}'. /// @@ -201,7 +200,7 @@ internal static string VersionOptionCannotBeCombinedWithOtherArguments(string op /// internal static string ExceptionHandlerHeader() => GetResourceString(Properties.Resources.ExceptionHandlerHeader); - +*/ /// /// Interpolates values into a localized string similar to Cannot parse argument '{0}' as expected type {1}.. /// @@ -233,7 +232,7 @@ internal static string ArgumentConversionCannotParseForOption(string value, stri internal static 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. /// @@ -252,8 +251,7 @@ private static string GetResourceString(string resourceString, params object[] f } return resourceString; } -/* + private static string GetOptionName(OptionResult optionResult) => optionResult.IdentifierToken?.Value ?? optionResult.Option.Name; -*/ } } diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index ad3fd82635..43590aedb2 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -16,29 +16,40 @@ namespace System.CommandLine /// public sealed class ParseResult { - /* private readonly CommandResult _rootCommandResult; +// TODO: unmatched tokens, invocation, completion +/* private readonly IReadOnlyList _unmatchedTokens; private CompletionContext? _completionContext; private readonly CliAction? _action; private readonly List? _preActions; +*/ internal ParseResult( CliConfiguration configuration, CommandResult rootCommandResult, CommandResult commandResult, List tokens, - List? unmatchedTokens, +// TODO: unmatched tokens +// List? unmatchedTokens, List? errors, - string? commandLineText = null, +// TODO: commandLineText should be string array + string? commandLineText = null //, +// TODO: invocation +/* CliAction? action = null, List? preActions = null) +*/ + ) { Configuration = configuration; _rootCommandResult = rootCommandResult; CommandResult = commandResult; + // TODO: invocation +/* _action = action; _preActions = preActions; +*/ // skip the root command when populating Tokens property if (tokens.Count > 1) @@ -55,11 +66,17 @@ internal ParseResult( } CommandLineText = commandLineText; - _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; + +// TODO: unmatched tokens +// _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; + Errors = errors is not null ? errors : Array.Empty(); } +// TODO: check that constructing empty ParseResult directly is correct +/* internal static ParseResult Empty() => new CliRootCommand().Parse(Array.Empty()); +*/ /// /// A result indicating the command specified in the command line input. @@ -81,10 +98,11 @@ internal ParseResult( /// public IReadOnlyList Errors { get; } + // TODO: don't expose tokens /// /// Gets the tokens identified while parsing command line input. /// - public IReadOnlyList Tokens { get; } + internal IReadOnlyList Tokens { get; } /// /// Holds the value of a complete command line input prior to splitting and tokenization, when provided. @@ -92,6 +110,8 @@ internal ParseResult( /// This will not be set when the parser is called from Program.Main. It is primarily used when calculating suggestions via the dotnet-suggest tool. internal string? CommandLineText { get; } + // TODO: CommandLineText, completion + /* /// /// Gets the list of tokens used on the command line that were not matched by the parser. /// @@ -106,7 +126,7 @@ public CompletionContext GetCompletionContext() => CommandLineText is null ? new CompletionContext(this) : new TextCompletionContext(this, CommandLineText); - + */ /// /// Gets the parsed or default value for the specified argument. /// @@ -134,8 +154,11 @@ CommandLineText is null public T? GetValue(string name) => RootCommandResult.GetValue(name); + // TODO: diagramming + /* /// public override string ToString() => ParseDiagramAction.Diagram(this).ToString(); + */ /// /// Gets the result, if any, for the specified argument. @@ -161,13 +184,15 @@ CommandLineText is null public OptionResult? GetResult(CliOption option) => _rootCommandResult.GetResult(option); +// TODO: Directives +/* /// /// Gets the result, if any, for the specified directive. /// /// The directive for which to find a result. /// A result for the specified directive, or if it was not provided. public DirectiveResult? GetResult(CliDirective directive) => _rootCommandResult.GetResult(directive); - +*/ /// /// Gets the result, if any, for the specified symbol. /// @@ -176,6 +201,8 @@ CommandLineText is null public SymbolResult? GetResult(CliSymbol symbol) => _rootCommandResult.SymbolResultTree.TryGetValue(symbol, out SymbolResult? result) ? result : null; +// TODO: completion, invocation +/* /// /// Gets completions based on a given parse result. /// diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/ArgumentResult.cs index a6c3042518..54a3f61481 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/ArgumentResult.cs @@ -132,7 +132,8 @@ private ArgumentConversionResult ValidateAndConvert(bool useValidators) { return ReportErrorIfNeeded(arityFailure); } - +// TODO: validators +/* // 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 @@ -150,7 +151,7 @@ private ArgumentConversionResult ValidateAndConvert(bool useValidators) return _conversionResult; } } - +*/ if (Parent!.UseDefaultValueFor(this)) { var defaultValue = Argument.GetDefaultValue(this); diff --git a/src/System.CommandLine/Parsing/CliParser.cs b/src/System.CommandLine/Parsing/CliParser.cs index 942390977f..fc159df716 100644 --- a/src/System.CommandLine/Parsing/CliParser.cs +++ b/src/System.CommandLine/Parsing/CliParser.cs @@ -18,8 +18,8 @@ public static class CliParser /// The string array typically passed to a program's Main method. /// The configuration on which the parser's grammar and behaviors are based. /// A providing details about the parse operation. - public static ParseResult Parse(CliCommand command, IReadOnlyList args, CliConfiguration? configuration = null) - => Parse(command, args, null, configuration); + public static ParseResult Parse(CliCommand rootCommand, IReadOnlyList args, CliConfiguration? configuration = null) + => Parse(rootCommand, args, null, configuration); /// /// Parses a command line string. @@ -29,8 +29,8 @@ public static ParseResult Parse(CliCommand command, IReadOnlyList args, /// The configuration on which the parser's grammar and behaviors are based. /// The command line string input will be split into tokens as if it had been passed on the command line. /// A providing details about the parse operation. - public static ParseResult Parse(CliCommand command, string commandLine, CliConfiguration? configuration = null) - => Parse(command, SplitCommandLine(commandLine).ToArray(), commandLine, configuration); + public static ParseResult Parse(CliCommand rootCommand, string commandLine, CliConfiguration? configuration = null) + => Parse(rootCommand, SplitCommandLine(commandLine).ToArray(), commandLine, configuration); /// /// Splits a string into a sequence of strings based on whitespace and quotation marks. @@ -136,7 +136,7 @@ string CurrentToken() } private static ParseResult Parse( - CliCommand command, + CliCommand rootCommand, IReadOnlyList arguments, string? rawInput, CliConfiguration? configuration) @@ -146,11 +146,11 @@ private static ParseResult Parse( throw new ArgumentNullException(nameof(arguments)); } - configuration ??= new CliConfiguration(command); + configuration ??= new CliConfiguration(rootCommand); CliTokenizer.Tokenize( arguments, - configuration.RootCommand, + rootCommand, inferRootCommand: rawInput is not null, configuration.EnablePosixBundling, out List tokens, @@ -158,6 +158,7 @@ private static ParseResult Parse( var operation = new ParseOperation( tokens, + rootCommand, configuration, tokenizationErrors, rawInput); diff --git a/src/System.CommandLine/Parsing/CliToken.cs b/src/System.CommandLine/Parsing/CliToken.cs index e268459468..76e7ca2d76 100644 --- a/src/System.CommandLine/Parsing/CliToken.cs +++ b/src/System.CommandLine/Parsing/CliToken.cs @@ -3,10 +3,12 @@ namespace System.CommandLine.Parsing { + // FIXME: should CliToken be public or internal? made internal for now + // FIXME: should CliToken be a struct? /// /// A unit of significant text on the command line. /// - public sealed class CliToken : IEquatable + internal sealed class CliToken : IEquatable { internal const int ImplicitPosition = -1; diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CommandResult.cs index 9c7c008fa6..de5fb1f507 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CommandResult.cs @@ -27,10 +27,11 @@ internal CommandResult( /// public CliCommand Command { get; } + // FIXME: should CliToken be public or internal? /// /// The token that was parsed to specify the command. /// - public CliToken IdentifierToken { get; } + internal CliToken IdentifierToken { get; } /// /// Child symbol results in the parse tree. @@ -48,12 +49,16 @@ internal void Validate(bool completeValidation) { if (completeValidation) { - if (Command.Action is null && Command.HasSubcommands) +// TODO: invocation +// if (Command.Action is null && Command.HasSubcommands) + if (Command.HasSubcommands) { SymbolResultTree.InsertFirstError( new ParseError(LocalizationResources.RequiredCommandWasNotProvided(), this)); } +// TODO: validators +/* if (Command.HasValidators) { int errorCountBefore = SymbolResultTree.ErrorCount; @@ -67,6 +72,7 @@ internal void Validate(bool completeValidation) return; } } +*/ } if (Command.HasOptions) @@ -87,7 +93,9 @@ private void ValidateOptions(bool completeValidation) { var option = options[i]; - if (!completeValidation && !(option.Recursive || option.Argument.HasDefaultValue || option is VersionOption)) +// TODO: VersionOption, recursive options +// if (!completeValidation && !(option.Recursive || option.Argument.HasDefaultValue || option is VersionOption)) + if (!completeValidation && !option.Argument.HasDefaultValue) { continue; } @@ -129,6 +137,8 @@ private void ValidateOptions(bool completeValidation) continue; } +// TODO: validators +/* if (optionResult.Option.HasValidators) { int errorsBefore = SymbolResultTree.ErrorCount; @@ -143,6 +153,7 @@ private void ValidateOptions(bool completeValidation) continue; } } +*/ _ = argumentResult.GetArgumentConversionResult(); } diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/OptionResult.cs index 805b30d9d4..d19ebff5b3 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/OptionResult.cs @@ -36,17 +36,20 @@ internal OptionResult( /// Implicit results commonly result from options having a default value. public bool Implicit => IdentifierToken is null || IdentifierToken.Implicit; +// TODO: make internal because exposes tokens /// /// The token that was parsed to specify the option. /// /// An identifier token is a token that matches either the option's name or one of its aliases. - public CliToken? IdentifierToken { get; } + internal CliToken? IdentifierToken { get; } +// TODO: do we even need IdentifierTokenCount +/* /// /// The number of occurrences of an identifier token matching the option. /// public int IdentifierTokenCount { get; internal set; } - +*/ /// public override string ToString() => $"{nameof(OptionResult)}: {IdentifierToken?.Value ?? Option.Name} {string.Join(" ", Tokens.Select(t => t.Value))}"; diff --git a/src/System.CommandLine/Parsing/ParseError.cs b/src/System.CommandLine/Parsing/ParseError.cs index a079b58c8b..5c5453a48e 100644 --- a/src/System.CommandLine/Parsing/ParseError.cs +++ b/src/System.CommandLine/Parsing/ParseError.cs @@ -8,6 +8,8 @@ namespace System.CommandLine.Parsing /// public sealed class ParseError { + // TODO: add position + // TODO: reevaluate whether we should be exposing a SymbolResult here internal ParseError( string message, SymbolResult? symbolResult = null) diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 2f272d4c39..d8d0d7e0a3 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -9,7 +9,6 @@ namespace System.CommandLine.Parsing { internal sealed class ParseOperation { - /* private readonly List _tokens; private readonly CliConfiguration _configuration; private readonly string? _rawInput; @@ -20,19 +19,23 @@ internal sealed class ParseOperation private CommandResult _innermostCommandResult; private bool _isHelpRequested; private bool _isTerminatingDirectiveSpecified; +// TODO: invocation +/* private CliAction? _primaryAction; private List? _preActions; - +*/ public ParseOperation( List tokens, + CliCommand rootCommand, CliConfiguration configuration, - List? tokenizeErrors, + List? tokenizationErrors, string? rawInput) { _tokens = tokens; _configuration = configuration; _rawInput = rawInput; - _symbolResultTree = new(_configuration.RootCommand, tokenizeErrors); + _symbolResultTree = new(rootCommand, tokenizationErrors); + _innermostCommandResult = _rootCommandResult = new CommandResult( _configuration.RootCommand, CurrentToken, @@ -55,15 +58,18 @@ private bool More(out CliTokenType currentTokenType) internal ParseResult Parse() { +// TODO: directives +/* ParseDirectives(); - +*/ ParseCommandChildren(); - if (!_isHelpRequested) { Validate(); } +// TODO: invocation +/* if (_primaryAction is null) { if (_symbolResultTree.ErrorCount > 0) @@ -71,17 +77,23 @@ internal ParseResult Parse() _primaryAction = new ParseErrorAction(); } } +*/ return new ( _configuration, _rootCommandResult, _innermostCommandResult, _tokens, - _symbolResultTree.UnmatchedTokens, +// TODO: unmatched tokens +// _symbolResultTree.UnmatchedTokens, _symbolResultTree.Errors, - _rawInput, + _rawInput +// TODO: invocation +/* _primaryAction, _preActions); +*/ + ); } private void ParseSubcommand() @@ -187,6 +199,8 @@ private void ParseOption() if (!_symbolResultTree.TryGetValue(option, out SymbolResult? symbolResult)) { +// TODO: invocation, directives, help +/* if (option.Action is not null) { // directives have a precedence over --help and --version @@ -207,7 +221,7 @@ private void ParseOption() } } } - +*/ optionResult = new OptionResult( option, _symbolResultTree, @@ -221,7 +235,8 @@ private void ParseOption() optionResult = (OptionResult)symbolResult; } - optionResult.IdentifierTokenCount++; +// TODO: IdentifierTokenCount +// optionResult.IdentifierTokenCount++; Advance(); @@ -290,7 +305,8 @@ private void ParseOptionArguments(OptionResult optionResult) } } } - +// TODO: directives +/* private void ParseDirectives() { while (More(out CliTokenType currentTokenType) && currentTokenType == CliTokenType.Directive) @@ -356,6 +372,7 @@ private void AddPreAction(CliAction action) _preActions.Add(action); } +*/ private void AddCurrentTokenToUnmatched() { @@ -381,15 +398,5 @@ private void Validate() currentResult = currentResult.Parent as CommandResult; } } - */ - public ParseOperation(List tokens, CliConfiguration configuration, List? tokenizationErrors, string? rawInput) - { - throw new NotImplementedException(); - } - - internal ParseResult Parse() - { - throw new NotImplementedException(); - } } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/SymbolResult.cs index dc0a0ba3e8..25b1c17b08 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/SymbolResult.cs @@ -10,6 +10,7 @@ namespace System.CommandLine.Parsing /// public abstract class SymbolResult { +// TODO: make this a property and protected if possible internal readonly SymbolResultTree SymbolResultTree; private protected List? _tokens; @@ -18,7 +19,8 @@ private protected SymbolResult(SymbolResultTree symbolResultTree, SymbolResult? SymbolResultTree = symbolResultTree; Parent = parent; } - +// TODO: this can be an extension method, do we need it? +/* /// /// The parse errors associated with this symbol result. /// @@ -43,25 +45,26 @@ public IEnumerable Errors } } } - +*/ /// /// The parent symbol result in the parse tree. /// public SymbolResult? Parent { get; } +// TODO: make internal because exposes tokens /// /// The list of tokens associated with this symbol result during parsing. /// - public IReadOnlyList Tokens => _tokens is not null ? _tokens : Array.Empty(); + internal IReadOnlyList Tokens => _tokens is not null ? _tokens : Array.Empty(); internal void AddToken(CliToken token) => (_tokens ??= new()).Add(token); +// TODO: made nonpublic, should we make public again? /// /// 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)); - + internal 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. /// @@ -83,13 +86,15 @@ public IEnumerable Errors /// An option result if the option was matched by the parser or has a default value; otherwise, null. public OptionResult? GetResult(CliOption option) => SymbolResultTree.GetResult(option); +// TODO: directives +/* /// /// Finds a result for the specific directive anywhere in the parse tree. /// /// The directive for which to find a result. /// A directive result if the directive was matched by the parser, null otherwise. public DirectiveResult? GetResult(CliDirective directive) => SymbolResultTree.GetResult(directive); - +*/ /// /// Finds a result for a symbol having the specified name anywhere in the parse tree. /// diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index f0d0fb3d7c..d235611d79 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -9,9 +9,11 @@ internal sealed class SymbolResultTree : Dictionary { private readonly CliCommand _rootCommand; internal List? Errors; +// TODO: unmatched tokens +/* internal List? UnmatchedTokens; +*/ private Dictionary? _symbolsByName; - internal SymbolResultTree( CliCommand rootCommand, List? tokenizeErrors) @@ -28,7 +30,6 @@ internal SymbolResultTree( } } } - internal int ErrorCount => Errors?.Count ?? 0; internal ArgumentResult? GetResult(CliArgument argument) @@ -40,9 +41,11 @@ internal SymbolResultTree( internal OptionResult? GetResult(CliOption option) => TryGetValue(option, out SymbolResult? result) ? (OptionResult)result : default; +//TODO: directives +/* internal DirectiveResult? GetResult(CliDirective directive) => TryGetValue(directive, out SymbolResult? result) ? (DirectiveResult)result : default; - +*/ internal IEnumerable GetChildren(SymbolResult parent) { if (parent is not ArgumentResult) @@ -58,11 +61,12 @@ 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(CliToken token, CommandResult commandResult, CommandResult rootCommandResult) { +/* +// TODO: unmatched tokens (UnmatchedTokens ??= new()).Add(token); if (commandResult.Command.TreatUnmatchedTokensAsErrors) @@ -72,8 +76,9 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com return; } +*/ AddError(new ParseError(LocalizationResources.UnrecognizedCommandOrArgument(token.Value), commandResult)); - } +// } } public SymbolResult? GetResult(string name) @@ -102,6 +107,11 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com return null; } +// TODO: symbolsbyname - this is inefficient +// results for some values may not be queried at all, dependent on other options +// so we could avoid using their value factories and adding them to the dictionary +// could we sort by name allowing us to do a binary search instead of allocating a dictionary? +// could we add codepaths that query for specific kinds of symbols so they don't have to search all symbols? private void PopulateSymbolsByName(CliCommand command) { if (command.HasArguments) diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 9d0de65a43..4595ebb603 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -25,6 +25,14 @@ + + + + + + + + @@ -38,14 +46,24 @@ + + + + + + - + + + + + From 49e43932d7e29afe9be204a4ba2bc1a94e205bb1 Mon Sep 17 00:00:00 2001 From: Jean Joeris Date: Sun, 18 Feb 2024 13:19:46 -0500 Subject: [PATCH 010/150] Updated ParserTest to use CliParser, commented out test --- src/System.CommandLine.Tests/ParserTests.cs | 2693 +++++++++-------- .../System.CommandLine.Tests.csproj | 1 + .../TokenizerTests.cs | 6 - .../Parsing/ArgumentResult.cs | 2 +- .../Parsing/ParseOperation.cs | 4 +- 5 files changed, 1374 insertions(+), 1332 deletions(-) diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 5618b41f0f..00024394ff 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -27,10 +27,16 @@ public void An_option_can_be_checked_by_object_instance() { var option = new CliOption("--flag"); var option2 = new CliOption("--flag2"); - var result = new CliRootCommand { option, option2 } - .Parse("--flag"); + var rootCommand = new CliRootCommand { option, option2 }; + var result = CliParser.Parse(rootCommand, ["--flag"]); result.GetResult(option).Should().NotBeNull(); + result.GetResult(option) + .Option + .Name + .Should() + .BeEquivalentTo("--flag"); + result.GetResult(option2).Should().BeNull(); } @@ -41,8 +47,9 @@ public void Two_options_are_parsed_correctly() var optionTwo = new CliOption("-t", "--two"); - var result = new CliRootCommand { optionOne, optionTwo }.Parse("-o -t"); - + var rootCommand = new CliRootCommand { optionOne, optionTwo }; + var result = CliParser.Parse(rootCommand, "-o -t"); + // TODO: consider more specific test result.GetResult(optionOne).Should().NotBeNull(); result.GetResult(optionTwo).Should().NotBeNull(); } @@ -52,7 +59,8 @@ public void Two_options_are_parsed_correctly() [InlineData("/")] public void When_a_token_is_just_a_prefix_then_an_error_is_returned(string prefix) { - var result = new CliRootCommand().Parse(prefix); + var rootCommand = new CliRootCommand(); + var result = CliParser.Parse(rootCommand, prefix); result.Errors .Select(e => e.Message) @@ -64,8 +72,9 @@ public void When_a_token_is_just_a_prefix_then_an_error_is_returned(string prefi public void Short_form_options_can_be_specified_using_equals_delimiter() { var option = new CliOption("-x"); + var rootCommand = new CliRootCommand { option }; - var result = new CliRootCommand { option }.Parse("-x=some-value"); + var result = CliParser.Parse(rootCommand, "-x=some-value"); result.Errors.Should().BeEmpty(); @@ -77,1549 +86,1587 @@ public void Long_form_options_can_be_specified_using_equals_delimiter() { var option = new CliOption("--hello"); - var result = new CliRootCommand { option }.Parse("--hello=there"); + var rootCommand = new CliRootCommand { option }; + var result = CliParser.Parse(rootCommand, "--hello=there"); result.Errors.Should().BeEmpty(); result.GetResult(option).Tokens.Should().ContainSingle(a => a.Value == "there"); } - [Fact] - public void Short_form_options_can_be_specified_using_colon_delimiter() - { - var option = new CliOption("-x"); - - var result = new CliRootCommand { option }.Parse("-x:some-value"); + [Fact] + public void Short_form_options_can_be_specified_using_colon_delimiter() + { + var option = new CliOption("-x"); - result.Errors.Should().BeEmpty(); + var rootCommand = new CliRootCommand { option }; + var result = CliParser.Parse(rootCommand,"-x:some-value"); - result.GetResult(option).Tokens.Should().ContainSingle(a => a.Value == "some-value"); - } + result.Errors.Should().BeEmpty(); - [Fact] - public void Long_form_options_can_be_specified_using_colon_delimiter() - { - var option = new CliOption("--hello"); + result.GetResult(option).Tokens.Should().ContainSingle(a => a.Value == "some-value"); + } - var result = new CliRootCommand { option }.Parse("--hello:there"); + [Fact] + public void Long_form_options_can_be_specified_using_colon_delimiter() + { + var option = new CliOption("--hello"); - result.Errors.Should().BeEmpty(); + var rootCommand = new CliRootCommand { option }; + var result = CliParser.Parse(rootCommand,"--hello:there"); - result.GetResult(option).Tokens.Should().ContainSingle(a => a.Value == "there"); - } + result.Errors.Should().BeEmpty(); - [Fact] - public void Option_short_forms_can_be_bundled() - { - var command = new CliCommand("the-command"); - command.Options.Add(new CliOption("-x")); - command.Options.Add(new CliOption("-y")); - command.Options.Add(new CliOption("-z")); + result.GetResult(option).Tokens.Should().ContainSingle(a => a.Value == "there"); + } - var result = command.Parse("the-command -xyz"); + [Fact] + public void Option_short_forms_can_be_bundled() + { + var command = new CliCommand("the-command"); + command.Options.Add(new CliOption("-x")); + command.Options.Add(new CliOption("-y")); + command.Options.Add(new CliOption("-z")); + + var result = CliParser.Parse(command, "the-command -xyz"); + + result.CommandResult + .Children + .Select(o => ((OptionResult)o).Option.Name) + .Should() + .BeEquivalentTo("-x", "-y", "-z"); + } - result.CommandResult - .Children - .Select(o => ((OptionResult)o).Option.Name) - .Should() - .BeEquivalentTo("-x", "-y", "-z"); - } + /* - [Fact] - public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off() - { - CliRootCommand rootCommand = new CliRootCommand() - { - new CliCommand("the-command") + [Fact] + public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off() { - new CliOption("-x"), - new CliOption("-y"), - new CliOption("-z") + // TODO: umatched tokens has been moved, fix + CliRootCommand rootCommand = new CliRootCommand() + { + new CliCommand("the-command") + { + new CliOption("-x"), + new CliOption("-y"), + new CliOption("-z") + } + }; + + CliConfiguration configuration = new (rootCommand) + { + EnablePosixBundling = false + }; + + var result = rootCommand.Parse("the-command -xyz", configuration); + + result.UnmatchedTokens + .Should() + .BeEquivalentTo("-xyz"); } - }; + */ - CliConfiguration configuration = new (rootCommand) - { - EnablePosixBundling = false - }; + [Fact] + public void Option_long_forms_do_not_get_unbundled() + { + CliCommand command = + new CliCommand("the-command") + { + new CliOption("--xyz"), + new CliOption("-x"), + new CliOption("-y"), + new CliOption("-z") + }; - var result = rootCommand.Parse("the-command -xyz", configuration); + var result = CliParser.Parse(command, "the-command --xyz"); - result.UnmatchedTokens - .Should() - .BeEquivalentTo("-xyz"); - } + result.CommandResult + .Children + .Select(o => ((OptionResult)o).Option.Name) + .Should() + .BeEquivalentTo("--xyz"); + } - [Fact] - public void Option_long_forms_do_not_get_unbundled() - { - CliCommand command = - new CliCommand("the-command") + [Fact] + public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_valid_for_the_current_command() { - new CliOption("--xyz"), - new CliOption("-x"), - new CliOption("-y"), - new CliOption("-z") - }; + var outer = new CliCommand("outer"); + outer.Options.Add(new CliOption("-a")); + var inner = new CliCommand("inner") + { + new CliArgument("arg") + }; + inner.Options.Add(new CliOption("-b")); + inner.Options.Add(new CliOption("-c")); + outer.Subcommands.Add(inner); + + ParseResult result = CliParser.Parse(outer, "outer inner -abc"); + + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("-abc"); + } - var result = command.Parse("the-command --xyz"); + [Fact] + public void Required_option_arguments_are_not_unbundled() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b"); + var optionC = new CliOption("-c"); - result.CommandResult - .Children - .Select(o => ((OptionResult)o).Option.Name) - .Should() - .BeEquivalentTo("--xyz"); - } + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; - [Fact] - public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_valid_for_the_current_command() - { - var outer = new CliCommand("outer"); - outer.Options.Add(new CliOption("-a")); - var inner = new CliCommand("inner") - { - new CliArgument("arg") - }; - inner.Options.Add(new CliOption("-b")); - inner.Options.Add(new CliOption("-c")); - outer.Subcommands.Add(inner); - - ParseResult result = outer.Parse("outer inner -abc"); - - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("-abc"); - } + var result = CliParser.Parse(command, "-a -bc"); - [Fact] - public void Required_option_arguments_are_not_unbundled() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b"); - var optionC = new CliOption("-c"); + result.GetResult(optionA) + .Tokens + .Should() + .ContainSingle(t => t.Value == "-bc"); + } - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + [Fact] + public void Last_bundled_option_can_accept_argument_with_no_separator() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; + var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; - var result = command.Parse("-a -bc"); + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; + + var result = CliParser.Parse(command, "-abcvalue"); + result.GetResult(optionA).Should().NotBeNull(); + result.GetResult(optionB).Should().NotBeNull(); + + result.GetResult(optionC) + .Tokens + .Should() + .ContainSingle(t => t.Value == "value"); + } - result.GetResult(optionA) - .Tokens - .Should() - .ContainSingle(t => t.Value == "-bc"); - } - - [Fact] - public void Last_bundled_option_can_accept_argument_with_no_separator() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; - var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; - - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; - - var result = command.Parse("-abcvalue"); - result.GetResult(optionA).Should().NotBeNull(); - result.GetResult(optionB).Should().NotBeNull(); - - result.GetResult(optionC) - .Tokens - .Should() - .ContainSingle(t => t.Value == "value"); - } + [Fact] + public void Last_bundled_option_can_accept_argument_with_equals_separator() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; + var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; - [Fact] - public void Last_bundled_option_can_accept_argument_with_equals_separator() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; - var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; - - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; - - var result = command.Parse("-abc=value"); - result.GetResult(optionA).Should().NotBeNull(); - result.GetResult(optionB).Should().NotBeNull(); - - result.GetResult(optionC) - .Tokens - .Should() - .ContainSingle(t => t.Value == "value"); - } + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; + + var result = CliParser.Parse(command, "-abc=value"); + result.GetResult(optionA).Should().NotBeNull(); + result.GetResult(optionB).Should().NotBeNull(); + + result.GetResult(optionC) + .Tokens + .Should() + .ContainSingle(t => t.Value == "value"); + } - [Fact] - public void Last_bundled_option_can_accept_argument_with_colon_separator() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; - var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; - - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; - - var result = command.Parse("-abc:value"); - result.GetResult(optionA).Should().NotBeNull(); - result.GetResult(optionB).Should().NotBeNull(); - - result.GetResult(optionC) - .Tokens - .Should() - .ContainSingle(t => t.Value == "value"); - } + [Fact] + public void Last_bundled_option_can_accept_argument_with_colon_separator() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; + var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; - [Fact] - public void Invalid_char_in_bundle_causes_rest_to_be_interpreted_as_value() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; - var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; - - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; - - var result = command.Parse("-abvcalue"); - result.GetResult(optionA).Should().NotBeNull(); - result.GetResult(optionB).Should().NotBeNull(); - - result.GetResult(optionB) - .Tokens - .Should() - .ContainSingle(t => t.Value == "vcalue"); + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; + + var result = CliParser.Parse(command, "-abc:value"); + result.GetResult(optionA).Should().NotBeNull(); + result.GetResult(optionB).Should().NotBeNull(); + + result.GetResult(optionC) + .Tokens + .Should() + .ContainSingle(t => t.Value == "value"); + } - result.GetResult(optionC).Should().BeNull(); - } + [Fact] + public void Invalid_char_in_bundle_causes_rest_to_be_interpreted_as_value() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; + var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; - [Fact] - public void Parser_root_Options_can_be_specified_multiple_times_and_their_arguments_are_collated() - { - var animalsOption = new CliOption("-a", "--animals"); - var vegetablesOption = new CliOption("-v", "--vegetables"); - var parser = new CliRootCommand - { - animalsOption, - vegetablesOption - }; - - var result = parser.Parse("-a cat -v carrot -a dog"); - - result.GetResult(animalsOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("cat", "dog"); + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; - result.GetResult(vegetablesOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("carrot"); - } + var result = CliParser.Parse(command, "-abvcalue"); + result.GetResult(optionA).Should().NotBeNull(); + result.GetResult(optionB).Should().NotBeNull(); - [Fact] - public void Options_can_be_specified_multiple_times_and_their_arguments_are_collated() - { - var animalsOption = new CliOption("-a", "--animals"); - animalsOption.AcceptOnlyFromAmong("dog", "cat", "sheep"); - var vegetablesOption = new CliOption("-v", "--vegetables"); - CliCommand command = - new CliCommand("the-command") { - animalsOption, - vegetablesOption - }; - - var result = command.Parse("the-command -a cat -v carrot -a dog"); - - result.GetResult(animalsOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("cat", "dog"); + result.GetResult(optionB) + .Tokens + .Should() + .ContainSingle(t => t.Value == "vcalue"); - result.GetResult(vegetablesOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("carrot"); - } + result.GetResult(optionC).Should().BeNull(); + } - [Fact] - public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_following_token_is_considered_an_argument_to_the_parent_command() - { - var animalsOption = new CliOption("-a", "--animals"); - var vegetablesOption = new CliOption("-v", "--vegetables"); + [Fact] + public void Parser_root_Options_can_be_specified_multiple_times_and_their_arguments_are_collated() + { + var animalsOption = new CliOption("-a", "--animals"); + var vegetablesOption = new CliOption("-v", "--vegetables"); + var rootCommand = new CliRootCommand + { + animalsOption, + vegetablesOption + }; + + var result = CliParser.Parse(rootCommand, "-a cat -v carrot -a dog"); + + result.GetResult(animalsOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("cat", "dog"); + + result.GetResult(vegetablesOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("carrot"); + } - CliCommand command = - new CliCommand("the-command") + /* + [Fact] + public void Options_can_be_specified_multiple_times_and_their_arguments_are_collated() { - animalsOption, - vegetablesOption, - new CliArgument("arg") - }; + // TODO: tests AcceptOnlyFromAmong, fix + var animalsOption = new CliOption("-a", "--animals"); + animalsOption.AcceptOnlyFromAmong("dog", "cat", "sheep"); + var vegetablesOption = new CliOption("-v", "--vegetables"); + CliCommand command = + new CliCommand("the-command") { + animalsOption, + vegetablesOption + }; - var result = command.Parse("the-command -a cat some-arg -v carrot"); + var result = command.Parse("the-command -a cat -v carrot -a dog"); - result.GetResult(animalsOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("cat"); + result.GetResult(animalsOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("cat", "dog"); - result.GetResult(vegetablesOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("carrot"); + result.GetResult(vegetablesOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("carrot"); + } + */ - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("some-arg"); - } + [Fact] + public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_following_token_is_considered_an_argument_to_the_parent_command() + { + var animalsOption = new CliOption("-a", "--animals"); - [Fact] - public void Command_with_multiple_options_is_parsed_correctly() - { - var command = new CliCommand("outer") - { - new CliOption("--inner1"), - new CliOption("--inner2") - }; + var vegetablesOption = new CliOption("-v", "--vegetables"); + + CliCommand command = + new CliCommand("the-command") + { + animalsOption, + vegetablesOption, + new CliArgument("arg") + }; - var result = command.Parse("outer --inner1 argument1 --inner2 argument2"); + var result = CliParser.Parse(command, "the-command -a cat some-arg -v carrot"); + + result.GetResult(animalsOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("cat"); + + result.GetResult(vegetablesOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("carrot"); + + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("some-arg"); + } - result.CommandResult - .Children - .Should() - .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner1" && - o.Tokens.Single().Value == "argument1"); - result.CommandResult - .Children - .Should() - .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner2" && - o.Tokens.Single().Value == "argument2"); - } + [Fact] + public void Command_with_multiple_options_is_parsed_correctly() + { + var command = new CliCommand("outer") + { + new CliOption("--inner1"), + new CliOption("--inner2") + }; + + var result = CliParser.Parse(command, "outer --inner1 argument1 --inner2 argument2"); + + result.CommandResult + .Children + .Should() + .ContainSingle(o => + ((OptionResult)o).Option.Name == "--inner1" && + o.Tokens.Single().Value == "argument1"); + result.CommandResult + .Children + .Should() + .ContainSingle(o => + ((OptionResult)o).Option.Name == "--inner2" && + o.Tokens.Single().Value == "argument2"); + } - [Fact] - public void Relative_order_of_arguments_and_options_within_a_command_does_not_matter() - { - var command = new CliCommand("move") - { - new CliArgument("arg"), - new CliOption("-X") - }; - - // option before args - ParseResult result1 = command.Parse( - "move -X the-arg-for-option-x ARG1 ARG2"); - - // option between two args - ParseResult result2 = command.Parse( - "move ARG1 -X the-arg-for-option-x ARG2"); - - // option after args - ParseResult result3 = command.Parse( - "move ARG1 ARG2 -X the-arg-for-option-x"); - - // all should be equivalent - result1.Should() - .BeEquivalentTo( - result2, - x => x.IgnoringCyclicReferences() - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); - result1.Should() - .BeEquivalentTo( - result3, - x => x.IgnoringCyclicReferences() - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); - } + [Fact] + public void Relative_order_of_arguments_and_options_within_a_command_does_not_matter() + { + var command = new CliCommand("move") + { + new CliArgument("arg"), + new CliOption("-X") + }; + + // option before args + ParseResult result1 = CliParser.Parse( + command, + "move -X the-arg-for-option-x ARG1 ARG2"); + + // option between two args + ParseResult result2 = CliParser.Parse( + command, + "move ARG1 -X the-arg-for-option-x ARG2"); + + // option after args + ParseResult result3 = CliParser.Parse( + command, + "move ARG1 ARG2 -X the-arg-for-option-x"); + + // all should be equivalent + result1.Should() + .BeEquivalentTo( + result2, + x => x.IgnoringCyclicReferences() + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); + result1.Should() + .BeEquivalentTo( + result3, + x => x.IgnoringCyclicReferences() + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); + } - [Theory] - [InlineData("--one 1 --many 1 --many 2")] - [InlineData("--one 1 --many 1 --many 2 arg1 arg2")] - [InlineData("--many 1 --one 1 --many 2")] - [InlineData("--many 2 --many 1 --one 1")] - [InlineData("[parse] --one 1 --many 1 --many 2")] - [InlineData("--one \"stuff in quotes\" this-is-arg1 \"this is arg2\"")] - [InlineData("not a valid command line --one 1")] - public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string commandLine) - { - var rawSplit = CliParser.SplitCommandLine(commandLine); + [Theory] + [InlineData("--one 1 --many 1 --many 2")] + [InlineData("--one 1 --many 1 --many 2 arg1 arg2")] + [InlineData("--many 1 --one 1 --many 2")] + [InlineData("--many 2 --many 1 --one 1")] + [InlineData("[parse] --one 1 --many 1 --many 2")] + [InlineData("--one \"stuff in quotes\" this-is-arg1 \"this is arg2\"")] + [InlineData("not a valid command line --one 1")] + public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string commandLine) + { + var rawSplit = CliParser.SplitCommandLine(commandLine); - var command = new CliCommand("the-command") - { - new CliArgument("arg"), - new CliOption("--one"), - new CliOption("--many") - }; + var command = new CliCommand("the-command") + { + new CliArgument("arg"), + new CliOption("--one"), + new CliOption("--many") + }; - var result = command.Parse(commandLine); + var result = CliParser.Parse(command, commandLine); - result.Tokens.Select(t => t.Value).Should().Equal(rawSplit); - } + result.Tokens.Select(t => t.Value).Should().Equal(rawSplit); + } - [Fact] - public void An_outer_command_with_the_same_name_does_not_capture() - { - var command = new CliCommand("one") - { - new CliCommand("two") - { - new CliCommand("three") - }, - new CliCommand("three") - }; - - ParseResult result = command.Parse("one two three"); - - result.Diagram().Should().Be("[ one [ two [ three ] ] ]"); - } + /* + [Fact] + public void An_outer_command_with_the_same_name_does_not_capture() + { + // TODO: uses Diagram, fix + var command = new CliCommand("one") + { + new CliCommand("two") + { + new CliCommand("three") + }, + new CliCommand("three") + }; + + ParseResult result = CliParser.Parse(command, "one two three"); + + result.Diagram().Should().Be("[ one [ two [ three ] ] ]"); + } - [Fact] - public void An_inner_command_with_the_same_name_does_not_capture() - { - var command = new CliCommand("one") - { - new CliCommand("two") - { - new CliCommand("three") - }, - new CliCommand("three") - }; - - ParseResult result = command.Parse("one three"); - - result.Diagram().Should().Be("[ one [ three ] ]"); - } + [Fact] + public void An_inner_command_with_the_same_name_does_not_capture() + { + // TODO: uses Diagram, fix + var command = new CliCommand("one") + { + new CliCommand("two") + { + new CliCommand("three") + }, + new CliCommand("three") + }; + + ParseResult result = CliParser.Parse(command, "one three"); + + result.Diagram().Should().Be("[ one [ three ] ]"); + } + */ - [Fact] - public void When_nested_commands_all_accept_arguments_then_the_nearest_captures_the_arguments() - { - var command = new CliCommand( - "outer") - { - new CliArgument("arg1"), - new CliCommand("inner") + [Fact] + public void When_nested_commands_all_accept_arguments_then_the_nearest_captures_the_arguments() { - new CliArgument("arg2") + var command = new CliCommand( + "outer") + { + new CliArgument("arg1"), + new CliCommand("inner") + { + new CliArgument("arg2") + } + }; + + var result = CliParser.Parse(command, "outer arg1 inner arg2"); + + result.CommandResult + .Parent + .Tokens.Select(t => t.Value) + .Should() + .BeEquivalentTo("arg1"); + + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("arg2"); } - }; - var result = command.Parse("outer arg1 inner arg2"); + /* + [Fact] + public void Nested_commands_with_colliding_names_cannot_both_be_applied() + { + // TODO: uses Diagram, fix + var command = new CliCommand("outer") + { + new CliArgument("arg1"), + new CliCommand("non-unique") + { + new CliArgument("arg2") + }, + new CliCommand("inner") + { + new CliArgument("arg3"), + new CliCommand("non-unique") + { + new CliArgument("arg4") + } + } + }; - result.CommandResult - .Parent - .Tokens.Select(t => t.Value) - .Should() - .BeEquivalentTo("arg1"); + ParseResult result = command.Parse("outer arg1 inner arg2 non-unique arg3 "); - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("arg2"); - } + result.Diagram().Should().Be("[ outer [ inner [ non-unique ] ] ]"); + } + */ - [Fact] - public void Nested_commands_with_colliding_names_cannot_both_be_applied() - { - var command = new CliCommand("outer") - { - new CliArgument("arg1"), - new CliCommand("non-unique") + [Fact] + public void When_child_option_will_not_accept_arg_then_parent_can() { - new CliArgument("arg2") - }, - new CliCommand("inner") - { - new CliArgument("arg3"), - new CliCommand("non-unique") - { - new CliArgument("arg4") - } + var option = new CliOption("-x"); + var command = new CliCommand("the-command") + { + option, + new CliArgument("arg") + }; + + var result = CliParser.Parse(command, "the-command -x the-argument"); + + var optionResult = result.GetResult(option); + optionResult.Tokens.Should().BeEmpty(); + result.CommandResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); } - }; - ParseResult result = command.Parse("outer arg1 inner arg2 non-unique arg3 "); + [Fact] + public void When_parent_option_will_not_accept_arg_then_child_can() + { + var option = new CliOption("-x"); + var command = new CliCommand("the-command") + { + option + }; - result.Diagram().Should().Be("[ outer [ inner [ non-unique ] ] ]"); - } + var result = CliParser.Parse(command, "the-command -x the-argument"); - [Fact] - public void When_child_option_will_not_accept_arg_then_parent_can() - { - var option = new CliOption("-x"); - var command = new CliCommand("the-command") - { - option, - new CliArgument("arg") - }; - - var result = command.Parse("the-command -x the-argument"); - - var optionResult = result.GetResult(option); - optionResult.Tokens.Should().BeEmpty(); - result.CommandResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); - } + result.GetResult(option).Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); + result.CommandResult.Tokens.Should().BeEmpty(); + } - [Fact] - public void When_parent_option_will_not_accept_arg_then_child_can() - { - var option = new CliOption("-x"); - var command = new CliCommand("the-command") - { - option - }; + [Fact] + public void Required_arguments_on_parent_commands_do_not_create_parse_errors_when_an_inner_command_is_specified() + { + var child = new CliCommand("child"); - var result = command.Parse("the-command -x the-argument"); + var parent = new CliCommand("parent") + { + new CliArgument("arg"), + child + }; - result.GetResult(option).Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); - result.CommandResult.Tokens.Should().BeEmpty(); - } + var result = CliParser.Parse(parent, "child"); - [Fact] - public void Required_arguments_on_parent_commands_do_not_create_parse_errors_when_an_inner_command_is_specified() - { - var child = new CliCommand("child"); + result.Errors.Should().BeEmpty(); + } - var parent = new CliCommand("parent") - { - new CliArgument("arg"), - child - }; + [Fact] + public void Required_arguments_on_grandparent_commands_do_not_create_parse_errors_when_an_inner_command_is_specified() + { + var grandchild = new CliCommand("grandchild"); - var result = parent.Parse("child"); + var grandparent = new CliCommand("grandparent") + { + new CliArgument("arg"), + new CliCommand("parent") + { + grandchild + } + }; - result.Errors.Should().BeEmpty(); - } + var result = CliParser.Parse(grandparent, "parent grandchild"); - [Fact] - public void Required_arguments_on_grandparent_commands_do_not_create_parse_errors_when_an_inner_command_is_specified() - { - var grandchild = new CliCommand("grandchild"); + result.Errors.Should().BeEmpty(); + } - var grandparent = new CliCommand("grandparent") - { - new CliArgument("arg"), - new CliCommand("parent") + [Fact] + public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_at_the_end_then_it_attaches_to_the_inner_command() { - grandchild + var outer = new CliCommand("outer") + { + new CliCommand("inner") + { + new CliOption("-x") + }, + new CliOption("-x") + }; + + ParseResult result = CliParser.Parse(outer, "outer inner -x"); + + result.CommandResult + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .AllBeAssignableTo(); + result.CommandResult + .Children + .Should() + .ContainSingle(o => ((OptionResult)o).Option.Name == "-x"); } - }; - var result = grandparent.Parse("parent grandchild"); + [Fact] + public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_in_between_then_it_attaches_to_the_outer_command() + { + var outer = new CliCommand("outer"); + outer.Options.Add(new CliOption("-x")); + var inner = new CliCommand("inner"); + inner.Options.Add(new CliOption("-x")); + outer.Subcommands.Add(inner); + + var result = CliParser.Parse(outer, "outer -x inner"); + + result.CommandResult + .Children + .Should() + .BeEmpty(); + result.CommandResult + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); + } - result.Errors.Should().BeEmpty(); - } + /* - [Fact] - public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_at_the_end_then_it_attaches_to_the_inner_command() - { - var outer = new CliCommand("outer") + [Fact] + // TODO: tests unmatched tokens, needs fix + public void Arguments_only_apply_to_the_nearest_command() + { + var outer = new CliCommand("outer") + { + new CliArgument("arg1"), + new CliCommand("inner") { - new CliCommand("inner") - { - new CliOption("-x") - }, - new CliOption("-x") - }; + new CliArgument("arg2") + } + }; + + ParseResult result = outer.Parse("outer inner arg1 arg2"); + + result.CommandResult + .Parent + .Tokens + .Should() + .BeEmpty(); + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("arg1"); + result.UnmatchedTokens + .Should() + .BeEquivalentTo("arg2"); + } + */ - ParseResult result = outer.Parse("outer inner -x"); + [Fact] + public void Options_only_apply_to_the_nearest_command() + { + var outerOption = new CliOption("-x"); + var innerOption = new CliOption("-x"); + + var outer = new CliCommand("outer") + { + new CliCommand("inner") + { + innerOption + }, + outerOption + }; + + var result = CliParser.Parse(outer, "outer inner -x one -x two"); + + result.RootCommandResult + .GetResult(outerOption) + .Should() + .BeNull(); + } - result.CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .AllBeAssignableTo(); - result.CommandResult - .Children - .Should() - .ContainSingle(o => ((OptionResult)o).Option.Name == "-x"); - } + [Fact] + public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_as_arguments() + { + var command = new CliCommand("the-command") + { + new CliCommand("complete") + { + new CliArgument("arg"), + new CliOption("--position") + } + }; - [Fact] - public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_in_between_then_it_attaches_to_the_outer_command() - { - var outer = new CliCommand("outer"); - outer.Options.Add(new CliOption("-x")); - var inner = new CliCommand("inner"); - inner.Options.Add(new CliOption("-x")); - outer.Subcommands.Add(inner); + ParseResult result = CliParser.Parse(command, new[] { "the-command", + "complete", + "--position", + "7", + "the-command" }); - var result = outer.Parse("outer -x inner"); + CommandResult completeResult = result.CommandResult; - result.CommandResult - .Children - .Should() - .BeEmpty(); - result.CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); - } + completeResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-command"); + } - [Fact] - public void Arguments_only_apply_to_the_nearest_command() - { - var outer = new CliCommand("outer") - { - new CliArgument("arg1"), - new CliCommand("inner") + [Fact] + public void Absolute_unix_style_paths_are_lexed_correctly() { - new CliArgument("arg2") - } - }; + const string commandText = + @"rm ""/temp/the file.txt"""; - ParseResult result = outer.Parse("outer inner arg1 arg2"); + CliCommand command = new ("rm") + { + new CliArgument("arg") + }; - result.CommandResult - .Parent - .Tokens - .Should() - .BeEmpty(); - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("arg1"); - result.UnmatchedTokens - .Should() - .BeEquivalentTo("arg2"); - } + var result = CliParser.Parse(command, commandText); - [Fact] - public void Options_only_apply_to_the_nearest_command() - { - var outerOption = new CliOption("-x"); - var innerOption = new CliOption("-x"); + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .OnlyContain(a => a == @"/temp/the file.txt"); + } - var outer = new CliCommand("outer") - { - new CliCommand("inner") - { - innerOption - }, - outerOption - }; + [Fact] + public void Absolute_Windows_style_paths_are_lexed_correctly() + { + const string commandText = + @"rm ""c:\temp\the file.txt\"""; - var result = outer.Parse("outer inner -x one -x two"); + CliCommand command = new("rm") + { + new CliArgument("arg") + }; - result.RootCommandResult - .GetResult(outerOption) - .Should() - .BeNull(); - } + ParseResult result = CliParser.Parse(command, commandText); - [Fact] - public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_as_arguments() - { - var command = new CliCommand("the-command") - { - new CliCommand("complete") - { - new CliArgument("arg"), - new CliOption("--position") + result.CommandResult + .Tokens + .Should() + .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); } - }; - ParseResult result = command.Parse(new[] { "the-command", - "complete", - "--position", - "7", - "the-command" }); + [Fact] + public void Commands_can_have_default_argument_values() + { + var argument = new CliArgument("the-arg") + { + DefaultValueFactory = (_) => "default" + }; - CommandResult completeResult = result.CommandResult; + var command = new CliCommand("command") + { + argument + }; - completeResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-command"); - } + ParseResult result = CliParser.Parse(command, "command"); - [Fact] - public void Absolute_unix_style_paths_are_lexed_correctly() - { - const string commandText = - @"rm ""/temp/the file.txt"""; + GetValue(result, argument) + .Should() + .Be("default"); + } - CliCommand command = new ("rm") - { - new CliArgument("arg") - }; + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_the_option_can_still_be_accessed_as_though_it_had_been_applied() + { + var command = new CliCommand("command"); + var option = new CliOption("-o", "--option") + { + DefaultValueFactory = (_) => "the-default" + }; + command.Options.Add(option); - var result = command.Parse(commandText); + ParseResult result = CliParser.Parse(command, "command"); - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .OnlyContain(a => a == @"/temp/the file.txt"); - } + result.GetResult(option).Should().NotBeNull(); + GetValue(result, option).Should().Be("the-default"); + } - [Fact] - public void Absolute_Windows_style_paths_are_lexed_correctly() - { - const string commandText = - @"rm ""c:\temp\the file.txt\"""; + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_the_option_result_is_implicit() + { + var option = new CliOption("-o", "--option") + { + DefaultValueFactory = (_) => "the-default" + }; - CliCommand command = new("rm") - { - new CliArgument("arg") - }; + var command = new CliCommand("command") + { + option + }; - ParseResult result = command.Parse(commandText); + var result = CliParser.Parse(command, "command"); - result.CommandResult - .Tokens - .Should() - .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); - } + result.GetResult(option) + .Implicit + .Should() + .BeTrue(); + } - [Fact] - public void Commands_can_have_default_argument_values() - { - var argument = new CliArgument("the-arg") - { - DefaultValueFactory = (_) => "default" - }; + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_there_are_no_tokens() + { + var option = new CliOption("-o") + { + DefaultValueFactory = (_) => "the-default" + }; - var command = new CliCommand("command") - { - argument - }; + var command = new CliCommand("command") + { + option + }; - ParseResult result = command.Parse("command"); + var result = CliParser.Parse(command, "command"); - GetValue(result, argument) - .Should() - .Be("default"); - } - - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_the_option_can_still_be_accessed_as_though_it_had_been_applied() - { - var command = new CliCommand("command"); - var option = new CliOption("-o", "--option") - { - DefaultValueFactory = (_) => "the-default" - }; - command.Options.Add(option); - - ParseResult result = command.Parse("command"); + result.GetResult(option) + .IdentifierToken + .Should() + .BeEquivalentTo(default(CliToken)); + } - result.GetResult(option).Should().NotBeNull(); - GetValue(result, option).Should().Be("the-default"); - } + [Fact] + public void When_an_argument_with_a_default_value_is_not_matched_then_there_are_no_tokens() + { + var argument = new CliArgument("o") + { + DefaultValueFactory = (_) => "the-default" + }; - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_the_option_result_is_implicit() - { - var option = new CliOption("-o", "--option") - { - DefaultValueFactory = (_) => "the-default" - }; + var command = new CliCommand("command") + { + argument + }; + var result = CliParser.Parse(command, "command"); + + result.GetResult(argument) + .Tokens + .Should() + .BeEmpty(); + } - var command = new CliCommand("command") - { - option - }; + [Fact] + public void Command_default_argument_value_does_not_override_parsed_value() + { + var argument = new CliArgument("the-arg") + { + DefaultValueFactory = (_) => new DirectoryInfo(Directory.GetCurrentDirectory()) + }; - var result = command.Parse("command"); + var command = new CliCommand("inner") + { + argument + }; - result.GetResult(option) - .Implicit - .Should() - .BeTrue(); - } + var result = CliParser.Parse(command, "the-directory"); - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_there_are_no_tokens() - { - var option = new CliOption("-o") - { - DefaultValueFactory = (_) => "the-default" - }; + GetValue(result, argument) + .Name + .Should() + .Be("the-directory"); + } - var command = new CliCommand("command") - { - option - }; + [Fact] + public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_tokens() + { + var outer = new CliCommand("outer") + { + new CliCommand("inner") + { + new CliArgument("arg") + { + Arity = ArgumentArity.OneOrMore + } + } + }; + + ParseResult result = CliParser.Parse(outer, "outer inner -p:RandomThing=random"); + + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("-p:RandomThing=random"); + } - var result = command.Parse("command"); + /* + [Fact] + public void The_default_behavior_of_unmatched_tokens_resulting_in_errors_can_be_turned_off() + { + // TODO: uses UnmatchedTokens, TreatUnmatchedTokensAsErrors, fix + var command = new CliCommand("the-command") + { + new CliArgument("arg") + }; + command.TreatUnmatchedTokensAsErrors = false; - result.GetResult(option) - .IdentifierToken - .Should() - .BeEquivalentTo(default(CliToken)); - } + ParseResult result = command.Parse("the-command arg1 arg2"); - [Fact] - public void When_an_argument_with_a_default_value_is_not_matched_then_there_are_no_tokens() - { - var argument = new CliArgument("o") - { - DefaultValueFactory = (_) => "the-default" - }; - - var command = new CliCommand("command") - { - argument - }; - var result = command.Parse("command"); - - result.GetResult(argument) - .Tokens - .Should() - .BeEmpty(); - } + result.Errors.Should().BeEmpty(); - [Fact] - public void Command_default_argument_value_does_not_override_parsed_value() - { - var argument = new CliArgument("the-arg") - { - DefaultValueFactory = (_) => new DirectoryInfo(Directory.GetCurrentDirectory()) - }; + result.UnmatchedTokens + .Should() + .BeEquivalentTo("arg2"); + } + */ - var command = new CliCommand("inner") - { - argument - }; + [Fact] + public void Option_and_Command_can_have_the_same_alias() + { + var innerCommand = new CliCommand("inner") + { + new CliArgument("arg1") + }; - var result = command.Parse("the-directory"); + var option = new CliOption("--inner"); - GetValue(result, argument) - .Name - .Should() - .Be("the-directory"); - } + var outerCommand = new CliCommand("outer") + { + innerCommand, + option, + new CliArgument("arg2") + }; + + CliParser.Parse(outerCommand, "outer inner") + .CommandResult + .Command + .Should() + .BeSameAs(innerCommand); + + CliParser.Parse(outerCommand, "outer --inner") + .CommandResult + .Command + .Should() + .BeSameAs(outerCommand); + + CliParser.Parse(outerCommand, "outer --inner inner") + .CommandResult + .Command + .Should() + .BeSameAs(innerCommand); + + CliParser.Parse(outerCommand, "outer --inner inner") + .CommandResult + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .Contain(o => ((OptionResult)o).Option == option); + } - [Fact] - public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_tokens() - { - var outer = new CliCommand("outer") - { - new CliCommand("inner") + [Fact] + public void Options_can_have_the_same_alias_differentiated_only_by_prefix() { - new CliArgument("arg") + var option1 = new CliOption("-a"); + var option2 = new CliOption("--a"); + + var rootCommand = new CliRootCommand { - Arity = ArgumentArity.OneOrMore - } + option1, + option2 + }; + + CliParser.Parse(rootCommand, "-a").CommandResult + .Children + .Select(s => ((OptionResult)s).Option) + .Should() + .BeEquivalentTo(option1); + CliParser.Parse(rootCommand, "--a").CommandResult + .Children + .Select(s => ((OptionResult)s).Option) + .Should() + .BeEquivalentTo(option2); } - }; - ParseResult result = outer.Parse("outer inner -p:RandomThing=random"); - - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("-p:RandomThing=random"); - } + [Theory] + [InlineData("-x", "\"hello\"")] + [InlineData("-x=", "\"hello\"")] + [InlineData("-x:", "\"hello\"")] + [InlineData("-x", "\"\"")] + [InlineData("-x=", "\"\"")] + [InlineData("-x:", "\"\"")] + public void When_an_option_argument_is_enclosed_in_double_quotes_its_value_retains_the_quotes( + string arg1, + string arg2) + { + var option = new CliOption("-x"); + var rootCommand = new CliRootCommand { option }; + var parseResult = CliParser.Parse(rootCommand, new[] { arg1, arg2 }); + + parseResult + .GetResult(option) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo(new[] { arg2 }); + } - [Fact] - public void The_default_behavior_of_unmatched_tokens_resulting_in_errors_can_be_turned_off() - { - var command = new CliCommand("the-command") - { - new CliArgument("arg") - }; - command.TreatUnmatchedTokensAsErrors = false; + [Fact] // https://github.com/dotnet/command-line-api/issues/1445 + public void Trailing_option_delimiters_are_ignored() + { + var rootCommand = new CliRootCommand + { + new CliCommand("subcommand") + { + new CliOption("--directory") + } + }; - ParseResult result = command.Parse("the-command arg1 arg2"); + var args = new[] { "subcommand", "--directory:", @"c:\" }; - result.Errors.Should().BeEmpty(); + var result = CliParser.Parse(rootCommand, args); - result.UnmatchedTokens - .Should() - .BeEquivalentTo("arg2"); - } + result.Errors.Should().BeEmpty(); - [Fact] - public void Option_and_Command_can_have_the_same_alias() - { - var innerCommand = new CliCommand("inner") - { - new CliArgument("arg1") - }; - - var option = new CliOption("--inner"); - - var outerCommand = new CliCommand("outer") - { - innerCommand, - option, - new CliArgument("arg2") - }; - - outerCommand.Parse("outer inner") - .CommandResult - .Command - .Should() - .BeSameAs(innerCommand); + result.Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentSequenceTo(new[] { "subcommand", "--directory", @"c:\" }); + } - outerCommand.Parse("outer --inner") - .CommandResult - .Command - .Should() - .BeSameAs(outerCommand); + [Theory] + [InlineData("-x -y")] + [InlineData("-x=-y")] + [InlineData("-x:-y")] + public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_options(string input) + { + var optionX = new CliOption("-x"); - outerCommand.Parse("outer --inner inner") - .CommandResult - .Command - .Should() - .BeSameAs(innerCommand); + var command = new CliCommand("command") + { + optionX, + new CliOption("-z") + }; - outerCommand.Parse("outer --inner inner") - .CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .Contain(o => ((OptionResult)o).Option == option); - } + var result = CliParser.Parse(command, input); - [Fact] - public void Options_can_have_the_same_alias_differentiated_only_by_prefix() - { - var option1 = new CliOption("-a"); - var option2 = new CliOption("--a"); - - var parser = new CliRootCommand - { - option1, - option2 - }; - - parser.Parse("-a").CommandResult - .Children - .Select(s => ((OptionResult)s).Option) - .Should() - .BeEquivalentTo(option1); - parser.Parse("--a").CommandResult - .Children - .Select(s => ((OptionResult)s).Option) - .Should() - .BeEquivalentTo(option2); - } + GetValue(result, optionX).Should().Be("-y"); + } - [Theory] - [InlineData("-x", "\"hello\"")] - [InlineData("-x=", "\"hello\"")] - [InlineData("-x:", "\"hello\"")] - [InlineData("-x", "\"\"")] - [InlineData("-x=", "\"\"")] - [InlineData("-x:", "\"\"")] - public void When_an_option_argument_is_enclosed_in_double_quotes_its_value_retains_the_quotes( - string arg1, - string arg2) - { - var option = new CliOption("-x"); + [Fact] + public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_bundled_options() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b"); + var optionC = new CliOption("-c"); - var parseResult = new CliRootCommand { option }.Parse(new[] { arg1, arg2 }); + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; - parseResult - .GetResult(option) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo(new[] { arg2 }); - } + var result = CliParser.Parse(command, "-a -bc"); - [Fact] // https://github.com/dotnet/command-line-api/issues/1445 - public void Trailing_option_delimiters_are_ignored() - { - var rootCommand = new CliRootCommand - { - new CliCommand("subcommand") - { - new CliOption("--directory") + GetValue(result, optionA).Should().Be("-bc"); + GetValue(result, optionB).Should().BeFalse(); + GetValue(result, optionC).Should().BeFalse(); } - }; - - var args = new[] { "subcommand", "--directory:", @"c:\" }; - var result = rootCommand.Parse(args); + [Fact] + public void Option_arguments_can_match_subcommands() + { + var optionA = new CliOption("-a"); + var rootCommand = new CliRootCommand + { + new CliCommand("subcommand"), + optionA + }; - result.Errors.Should().BeEmpty(); + var result = CliParser.Parse(rootCommand, "-a subcommand"); - result.Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentSequenceTo(new[] { "subcommand", "--directory", @"c:\" }); - } + GetValue(result, optionA).Should().Be("subcommand"); + result.CommandResult.Command.Should().BeSameAs(rootCommand); + } - [Theory] - [InlineData("-x -y")] - [InlineData("-x=-y")] - [InlineData("-x:-y")] - public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_options(string input) - { - var optionX = new CliOption("-x"); + [Fact] + public void Arguments_can_match_subcommands() + { + var argument = new CliArgument("arg"); + var subcommand = new CliCommand("subcommand") + { + argument + }; + var rootCommand = new CliRootCommand + { + subcommand + }; - var command = new CliCommand("command") - { - optionX, - new CliOption("-z") - }; + var result = CliParser.Parse(rootCommand, "subcommand one two three subcommand four"); - var result = command.Parse(input); + result.CommandResult.Command.Should().BeSameAs(subcommand); - GetValue(result, optionX).Should().Be("-y"); - } - - [Fact] - public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_bundled_options() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b"); - var optionC = new CliOption("-c"); - - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; - - var result = command.Parse("-a -bc"); - - GetValue(result, optionA).Should().Be("-bc"); - GetValue(result, optionB).Should().BeFalse(); - GetValue(result, optionC).Should().BeFalse(); - } + GetValue(result, argument) + .Should() + .BeEquivalentSequenceTo("one", "two", "three", "subcommand", "four"); + } - [Fact] - public void Option_arguments_can_match_subcommands() - { - var optionA = new CliOption("-a"); - var root = new CliRootCommand - { - new CliCommand("subcommand"), - optionA - }; + [Theory] + [InlineData("-x=-y")] + [InlineData("-x:-y")] + public void Option_arguments_can_match_the_aliases_of_sibling_options_when_non_space_argument_delimiter_is_used(string input) + { + var optionX = new CliOption("-x"); - var result = root.Parse("-a subcommand"); + var command = new CliCommand("command") + { + optionX, + new CliOption("-y") + }; - GetValue(result, optionA).Should().Be("subcommand"); - result.CommandResult.Command.Should().BeSameAs(root); - } + var result = CliParser.Parse(command, input); - [Fact] - public void Arguments_can_match_subcommands() - { - var argument = new CliArgument("arg"); - var subcommand = new CliCommand("subcommand") - { - argument - }; - var root = new CliRootCommand - { - subcommand - }; + result.Errors.Should().BeEmpty(); + GetValue(result, optionX).Should().Be("-y"); + } - var result = root.Parse("subcommand one two three subcommand four"); + [Fact] + public void Single_option_arguments_that_match_option_aliases_are_parsed_correctly() + { + var optionX = new CliOption("-x"); - result.CommandResult.Command.Should().BeSameAs(subcommand); + var command = new CliRootCommand + { + optionX + }; - GetValue(result, argument) - .Should() - .BeEquivalentSequenceTo("one", "two", "three", "subcommand", "four"); - } + var result = CliParser.Parse(command, "-x -x"); - [Theory] - [InlineData("-x=-y")] - [InlineData("-x:-y")] - public void Option_arguments_can_match_the_aliases_of_sibling_options_when_non_space_argument_delimiter_is_used(string input) - { - var optionX = new CliOption("-x"); + GetValue(result, optionX).Should().Be("-x"); + } - var command = new CliCommand("command") - { - optionX, - new CliOption("-y") - }; + [Theory] + [InlineData("-x -y")] + [InlineData("-x true -y")] + [InlineData("-x:true -y")] + [InlineData("-x=true -y")] + [InlineData("-x -y true")] + [InlineData("-x true -y true")] + [InlineData("-x:true -y:true")] + [InlineData("-x=true -y:true")] + public void Boolean_options_are_not_greedy(string commandLine) + { + var optX = new CliOption("-x"); + var optY = new CliOption("-y"); - var result = command.Parse(input); + var root = new CliRootCommand() + { + optX, + optY, + }; - result.Errors.Should().BeEmpty(); - GetValue(result, optionX).Should().Be("-y"); - } + var result = CliParser.Parse(root, commandLine); - [Fact] - public void Single_option_arguments_that_match_option_aliases_are_parsed_correctly() - { - var optionX = new CliOption("-x"); + result.Errors.Should().BeEmpty(); - var command = new CliRootCommand - { - optionX - }; + GetValue(result, optX).Should().BeTrue(); + GetValue(result, optY).Should().BeTrue(); + } - var result = command.Parse("-x -x"); + [Fact] + public void Multiple_option_arguments_that_match_multiple_arity_option_aliases_are_parsed_correctly() + { + var optionX = new CliOption("-x"); + var optionY = new CliOption("-y"); - GetValue(result, optionX).Should().Be("-x"); - } + var command = new CliRootCommand + { + optionX, + optionY + }; - [Theory] - [InlineData("-x -y")] - [InlineData("-x true -y")] - [InlineData("-x:true -y")] - [InlineData("-x=true -y")] - [InlineData("-x -y true")] - [InlineData("-x true -y true")] - [InlineData("-x:true -y:true")] - [InlineData("-x=true -y:true")] - public void Boolean_options_are_not_greedy(string commandLine) - { - var optX = new CliOption("-x"); - var optY = new CliOption("-y"); + var result = CliParser.Parse(command, "-x -x -x -y -y -x -y -y -y -x -x -y"); - var root = new CliRootCommand("parent") - { - optX, - optY, - }; + GetValue(result, optionX).Should().BeEquivalentTo(new[] { "-x", "-y", "-y" }); + GetValue(result, optionY).Should().BeEquivalentTo(new[] { "-x", "-y", "-x" }); + } - var result = root.Parse(commandLine); + [Fact] + public void Bundled_option_arguments_that_match_option_aliases_are_parsed_correctly() + { + var optionX = new CliOption("-x"); + var optionY = new CliOption("-y"); - result.Errors.Should().BeEmpty(); + var command = new CliRootCommand + { + optionX, + optionY + }; - GetValue(result, optX).Should().BeTrue(); - GetValue(result, optY).Should().BeTrue(); - } + var result = CliParser.Parse(command, "-yxx"); - [Fact] - public void Multiple_option_arguments_that_match_multiple_arity_option_aliases_are_parsed_correctly() - { - var optionX = new CliOption("-x"); - var optionY = new CliOption("-y"); + GetValue(result, optionX).Should().Be("x"); + } - var command = new CliRootCommand - { - optionX, - optionY - }; + [Fact] + public void Argument_name_is_not_matched_as_a_token() + { + var nameArg = new CliArgument("name"); + var columnsArg = new CliArgument>("columns"); - var result = command.Parse("-x -x -x -y -y -x -y -y -y -x -x -y"); + var command = new CliCommand("add") + { + nameArg, + columnsArg + }; - GetValue(result, optionX).Should().BeEquivalentTo(new[] { "-x", "-y", "-y" }); - GetValue(result, optionY).Should().BeEquivalentTo(new[] { "-x", "-y", "-x" }); - } + var result = CliParser.Parse(command, "name one two three"); - [Fact] - public void Bundled_option_arguments_that_match_option_aliases_are_parsed_correctly() - { - var optionX = new CliOption("-x"); - var optionY = new CliOption("-y"); + GetValue(result, nameArg).Should().Be("name"); + GetValue(result, columnsArg).Should().BeEquivalentTo("one", "two", "three"); + } - var command = new CliRootCommand - { - optionX, - optionY - }; + [Fact] + public void Option_aliases_do_not_need_to_be_prefixed() + { + var option = new CliOption("noprefix"); - var result = command.Parse("-yxx"); + var rootCommand = new CliRootCommand { option }; + var result = CliParser.Parse(rootCommand, "noprefix"); - GetValue(result, optionX).Should().Be("x"); - } + result.GetResult(option).Should().NotBeNull(); + } - [Fact] - public void Argument_name_is_not_matched_as_a_token() - { - var nameArg = new CliArgument("name"); - var columnsArg = new CliArgument>("columns"); + [Fact] + public void Boolean_options_with_no_argument_specified_do_not_match_subsequent_arguments() + { + var option = new CliOption("-v"); - var command = new CliCommand("add", "Adds a new series") - { - nameArg, - columnsArg - }; + var command = new CliCommand("command") + { + option + }; - var result = command.Parse("name one two three"); + var result = CliParser.Parse(command, "-v an-argument"); - GetValue(result, nameArg).Should().Be("name"); - GetValue(result, columnsArg).Should().BeEquivalentTo("one", "two", "three"); - } + GetValue(result, option).Should().BeTrue(); + } - [Fact] - public void Option_aliases_do_not_need_to_be_prefixed() - { - var option = new CliOption("noprefix"); + /* + [Fact] + public void When_a_command_line_has_unmatched_tokens_they_are_not_applied_to_subsequent_options() + { + // TODO: uses TreatUnmatchedTokensAsErrors, fix + var command = new CliCommand("command") + { + TreatUnmatchedTokensAsErrors = false + }; + var optionX = new CliOption("-x"); + command.Options.Add(optionX); + var optionY = new CliOption("-y"); + command.Options.Add(optionY); + + var result = command.Parse("-x 23 unmatched-token -y 42"); + + GetValue(result, optionX).Should().Be("23"); + GetValue(result, optionY).Should().Be("42"); + result.UnmatchedTokens.Should().BeEquivalentTo("unmatched-token"); + } - var result = new CliRootCommand { option }.Parse("noprefix"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void When_a_command_line_has_unmatched_tokens_the_parse_result_action_should_depend_on_parsed_command_TreatUnmatchedTokensAsErrors(bool treatUnmatchedTokensAsErrors) + { + // TODO: uses TreatUnmatchedTokensAsErrors, fix + CliRootCommand rootCommand = new(); + CliCommand subcommand = new("vstest") + { + new CliOption("--Platform"), + new CliOption("--Framework"), + new CliOption("--logger") + }; + subcommand.TreatUnmatchedTokensAsErrors = treatUnmatchedTokensAsErrors; + rootCommand.Subcommands.Add(subcommand); - result.GetResult(option).Should().NotBeNull(); - } + var result = rootCommand.Parse("vstest test1.dll test2.dll"); - [Fact] - public void Boolean_options_with_no_argument_specified_do_not_match_subsequent_arguments() - { - var option = new CliOption("-v"); + result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); - var command = new CliCommand("command") - { - option - }; + if (treatUnmatchedTokensAsErrors) + { + result.Errors.Should().NotBeEmpty(); + result.Action.Should().NotBeSameAs(result.CommandResult.Command.Action); + } + else + { + result.Errors.Should().BeEmpty(); + result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + } + } - var result = command.Parse("-v an-argument"); + [Fact] + public void RootCommand_TreatUnmatchedTokensAsErrors_set_to_false_has_precedence_over_subcommands() + { + // TODO: uses TreatUnmatchedTokensAsErrors, fix + CliRootCommand rootCommand = new(); + rootCommand.TreatUnmatchedTokensAsErrors = false; + CliCommand subcommand = new("vstest") + { + new CliOption("--Platform"), + new CliOption("--Framework"), + new CliOption("--logger") + }; + subcommand.TreatUnmatchedTokensAsErrors = true; // the default, set to true to make it explicit + rootCommand.Subcommands.Add(subcommand); - GetValue(result, option).Should().BeTrue(); - } + var result = rootCommand.Parse("vstest test1.dll test2.dll"); - [Fact] - public void When_a_command_line_has_unmatched_tokens_they_are_not_applied_to_subsequent_options() - { - var command = new CliCommand("command") - { - TreatUnmatchedTokensAsErrors = false - }; - var optionX = new CliOption("-x"); - command.Options.Add(optionX); - var optionY = new CliOption("-y"); - command.Options.Add(optionY); - - var result = command.Parse("-x 23 unmatched-token -y 42"); - - GetValue(result, optionX).Should().Be("23"); - GetValue(result, optionY).Should().Be("42"); - result.UnmatchedTokens.Should().BeEquivalentTo("unmatched-token"); - } + result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void When_a_command_line_has_unmatched_tokens_the_parse_result_action_should_depend_on_parsed_command_TreatUnmatchedTokensAsErrors(bool treatUnmatchedTokensAsErrors) - { - CliRootCommand rootCommand = new(); - CliCommand subcommand = new("vstest") - { - new CliOption("--Platform"), - new CliOption("--Framework"), - new CliOption("--logger") - }; - subcommand.TreatUnmatchedTokensAsErrors = treatUnmatchedTokensAsErrors; - rootCommand.Subcommands.Add(subcommand); - - var result = rootCommand.Parse("vstest test1.dll test2.dll"); - - result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); - - if (treatUnmatchedTokensAsErrors) - { - result.Errors.Should().NotBeEmpty(); - result.Action.Should().NotBeSameAs(result.CommandResult.Command.Action); - } - else - { - result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); - } - } + result.Errors.Should().BeEmpty(); + result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + } + */ [Fact] - public void RootCommand_TreatUnmatchedTokensAsErrors_set_to_false_has_precedence_over_subcommands() - { - CliRootCommand rootCommand = new(); - rootCommand.TreatUnmatchedTokensAsErrors = false; - CliCommand subcommand = new("vstest") - { - new CliOption("--Platform"), - new CliOption("--Framework"), - new CliOption("--logger") - }; - subcommand.TreatUnmatchedTokensAsErrors = true; // the default, set to true to make it explicit - rootCommand.Subcommands.Add(subcommand); - - var result = rootCommand.Parse("vstest test1.dll test2.dll"); - - result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); + public void Parse_can_not_be_called_with_null_args() + { + Action passNull = () => CliParser.Parse(new CliRootCommand(), args: null); - result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); - } + passNull.Should().Throw(); + } - [Fact] - public void Parse_can_not_be_called_with_null_args() - { - Action passNull = () => new CliRootCommand().Parse(args: null); + [Fact] + public void Command_argument_arity_can_be_a_fixed_value_greater_than_1() + { + var argument = new CliArgument("arg") + { + Arity = new ArgumentArity(3, 3) + }; + var command = new CliCommand("the-command") + { + argument + }; + + CliParser.Parse(command, "1 2 3") + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument), + new CliToken("2", CliTokenType.Argument, argument), + new CliToken("3", CliTokenType.Argument, argument)); + } - passNull.Should().Throw(); - } + [Fact] + public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_than_1() + { + var argument = new CliArgument("arg") + { + Arity = new ArgumentArity(3, 5) + }; + var command = new CliCommand("the-command") + { + argument + }; + + CliParser.Parse(command, "1 2 3") + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument), + new CliToken("2", CliTokenType.Argument, argument), + new CliToken("3", CliTokenType.Argument, argument)); + CliParser.Parse(command, "1 2 3 4 5") + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument), + new CliToken("2", CliTokenType.Argument, argument), + new CliToken("3", CliTokenType.Argument, argument), + new CliToken("4", CliTokenType.Argument, argument), + new CliToken("5", CliTokenType.Argument, argument)); + } - [Fact] - public void Command_argument_arity_can_be_a_fixed_value_greater_than_1() - { - var argument = new CliArgument("arg") - { - Arity = new ArgumentArity(3, 3) - }; - var command = new CliCommand("the-command") - { - argument - }; - - command.Parse("1 2 3") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument), - new CliToken("2", CliTokenType.Argument, argument), - new CliToken("3", CliTokenType.Argument, argument)); - } + [Fact] + public void When_command_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() + { + var command = new CliCommand("the-command") + { + new CliArgument("arg") + { + Arity = new ArgumentArity(2, 3) + } + }; - [Fact] - public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_than_1() - { - var argument = new CliArgument("arg") - { - Arity = new ArgumentArity(3, 5) - }; - var command = new CliCommand("the-command") - { - argument - }; - - command.Parse("1 2 3") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument), - new CliToken("2", CliTokenType.Argument, argument), - new CliToken("3", CliTokenType.Argument, argument)); - command.Parse("1 2 3 4 5") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument), - new CliToken("2", CliTokenType.Argument, argument), - new CliToken("3", CliTokenType.Argument, argument), - new CliToken("4", CliTokenType.Argument, argument), - new CliToken("5", CliTokenType.Argument, argument)); - } + var result = CliParser.Parse(command, "1"); - [Fact] - public void When_command_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() - { - var command = new CliCommand("the-command") - { - new CliArgument("arg") - { - Arity = new ArgumentArity(2, 3) + result.Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(command.Arguments[0]))); } - }; - var result = command.Parse("1"); + [Fact] + public void When_command_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() + { + var command = new CliCommand("the-command") + { + new CliArgument("arg") + { + Arity = new ArgumentArity(2, 3) + } + }; - result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(command.Arguments[0]))); - } + ParseResult parseResult = CliParser.Parse(command, "1 2 3 4"); - [Fact] - public void When_command_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() - { - var command = new CliCommand("the-command") - { - new CliArgument("arg") - { - Arity = new ArgumentArity(2, 3) + parseResult + .Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); } - }; - - ParseResult parseResult = command.Parse("1 2 3 4"); - parseResult - .Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); - } + [Fact] + public void Option_argument_arity_can_be_a_fixed_value_greater_than_1() + { + var option = new CliOption("-x") { Arity = new ArgumentArity(3, 3)}; - [Fact] - public void Option_argument_arity_can_be_a_fixed_value_greater_than_1() - { - var option = new CliOption("-x") { Arity = new ArgumentArity(3, 3)}; - - var command = new CliCommand("the-command") - { - option - }; - - command.Parse("-x 1 -x 2 -x 3") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default), - new CliToken("2", CliTokenType.Argument, default), - new CliToken("3", CliTokenType.Argument, default)); - } + var command = new CliCommand("the-command") + { + option + }; + + CliParser.Parse(command, "-x 1 -x 2 -x 3") + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default), + new CliToken("2", CliTokenType.Argument, default), + new CliToken("3", CliTokenType.Argument, default)); + } - [Fact] - public void Option_argument_arity_can_be_a_range_with_a_lower_bound_greater_than_1() - { - var option = new CliOption("-x") { Arity = new ArgumentArity(3, 5) }; - - var command = new CliCommand("the-command") - { - option - }; - - command.Parse("-x 1 -x 2 -x 3") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default), - new CliToken("2", CliTokenType.Argument, default), - new CliToken("3", CliTokenType.Argument, default)); - command.Parse("-x 1 -x 2 -x 3 -x 4 -x 5") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default), - new CliToken("2", CliTokenType.Argument, default), - new CliToken("3", CliTokenType.Argument, default), - new CliToken("4", CliTokenType.Argument, default), - new CliToken("5", CliTokenType.Argument, default)); - } + [Fact] + public void Option_argument_arity_can_be_a_range_with_a_lower_bound_greater_than_1() + { + var option = new CliOption("-x") { Arity = new ArgumentArity(3, 5) }; - [Fact] - public void When_option_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() - { - var option = new CliOption("-x") - { - Arity = new ArgumentArity(2, 3) - }; + var command = new CliCommand("the-command") + { + option + }; + + CliParser.Parse(command, "-x 1 -x 2 -x 3") + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default), + new CliToken("2", CliTokenType.Argument, default), + new CliToken("3", CliTokenType.Argument, default)); + CliParser.Parse(command, "-x 1 -x 2 -x 3 -x 4 -x 5") + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default), + new CliToken("2", CliTokenType.Argument, default), + new CliToken("3", CliTokenType.Argument, default), + new CliToken("4", CliTokenType.Argument, default), + new CliToken("5", CliTokenType.Argument, default)); + } - var command = new CliCommand("the-command") - { - option - }; + [Fact] + public void When_option_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() + { + var option = new CliOption("-x") + { + Arity = new ArgumentArity(2, 3) + }; - var result = command.Parse("-x 1"); + var command = new CliCommand("the-command") + { + option + }; - result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(option))); - } + var result = CliParser.Parse(command, "-x 1"); - [Fact] - public void When_option_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() - { - var command = new CliCommand("the-command") - { - new CliOption("-x") { Arity = new ArgumentArity(2, 3)} - }; - - command.Parse("-x 1 2 3 4") - .Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); - } - - [Fact] - public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_option() - { - var rootCommand = new CliCommand("jdbc"); - rootCommand.Add(new CliOption("url")); - var result = rootCommand.Parse("jdbc url \"jdbc:sqlserver://10.0.0.2;databaseName=main\""); + result.Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(option))); + } - result.Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("url", - "jdbc:sqlserver://10.0.0.2;databaseName=main"); - } + [Fact] + public void When_option_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() + { + var command = new CliCommand("the-command") + { + new CliOption("-x") { Arity = new ArgumentArity(2, 3)} + }; + + CliParser.Parse(command, "-x 1 2 3 4") + .Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); + } - [Fact] - public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() - { - // Tests bug identified in https://github.com/dotnet/command-line-api/issues/997 + [Fact] + public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_option() + { + var rootCommand = new CliCommand("jdbc"); + rootCommand.Add(new CliOption("url")); + var result = CliParser.Parse(rootCommand, "jdbc url \"jdbc:sqlserver://10.0.0.2;databaseName=main\""); + + result.Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("url", + "jdbc:sqlserver://10.0.0.2;databaseName=main"); + } + /* + [Fact] + public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() + { + // TODO: uses GetCompletions, fix + // Tests bug identified in https://github.com/dotnet/command-line-api/issues/997 - var argument1 = new CliArgument("arg1"); + var argument1 = new CliArgument("arg1"); - var argument2 = new CliArgument("arg2"); + var argument2 = new CliArgument("arg2"); - var command = new CliCommand("subcommand") - { - argument1, - argument2 - }; + var command = new CliCommand("subcommand") + { + argument1, + argument2 + }; - var rootCommand = new CliRootCommand - { - command - }; + var rootCommand = new CliRootCommand + { + command + }; - var parseResult = rootCommand.Parse("subcommand arg1 arg2"); + var parseResult = rootCommand.Parse("subcommand arg1 arg2"); - Action act = () => parseResult.GetCompletions(); - act.Should().NotThrow(); - } + Action act = () => parseResult.GetCompletions(); + act.Should().NotThrow(); + } + */ - [Theory] // https://github.com/dotnet/command-line-api/issues/1551, https://github.com/dotnet/command-line-api/issues/1533 - [InlineData("--exec-prefix", "")] - [InlineData("--exec-prefix:", "")] - [InlineData("--exec-prefix=", "")] - public void Parsed_value_of_empty_string_arg_is_an_empty_string(string arg1, string arg2) - { - var option = new CliOption("--exec-prefix") - { - DefaultValueFactory = _ => "/usr/local" - }; + [Theory] // https://github.com/dotnet/command-line-api/issues/1551, https://github.com/dotnet/command-line-api/issues/1533 + [InlineData("--exec-prefix", "")] + [InlineData("--exec-prefix:", "")] + [InlineData("--exec-prefix=", "")] + public void Parsed_value_of_empty_string_arg_is_an_empty_string(string arg1, string arg2) + { + var option = new CliOption("--exec-prefix") + { + DefaultValueFactory = _ => "/usr/local" + }; - var rootCommand = new CliRootCommand - { - option - }; + var rootCommand = new CliRootCommand + { + option + }; - var result = rootCommand.Parse(new[] { arg1, arg2 }); + var result = CliParser.Parse(rootCommand, new[] { arg1, arg2 }); - GetValue(result, option).Should().BeEmpty(); - } + GetValue(result, option).Should().BeEmpty(); + } + /* + */ } } diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index fece53366d..66b2d14b22 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/src/System.CommandLine.Tests/TokenizerTests.cs b/src/System.CommandLine.Tests/TokenizerTests.cs index fe1a36870e..8f162059a6 100644 --- a/src/System.CommandLine.Tests/TokenizerTests.cs +++ b/src/System.CommandLine.Tests/TokenizerTests.cs @@ -24,12 +24,6 @@ public void The_tokenizer_is_accessible() List errors = null; CliTokenizer.Tokenize(args,command,false, true, out tokens, out errors); - //result.GetResult(animalsOption) - // .Tokens - // .Select(t => t.Value) - // .Should() - // .BeEquivalentTo("cat", "dog"); - tokens .Skip(1) .Select(t => t.Value) diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/ArgumentResult.cs index 54a3f61481..561ce36a02 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/ArgumentResult.cs @@ -120,7 +120,7 @@ public void OnlyTake(int numberOfTokens) public override string ToString() => $"{nameof(ArgumentResult)} {Argument.Name}: {string.Join(" ", Tokens.Select(t => $"<{t.Value}>"))}"; /// - public override void AddError(string errorMessage) + internal override void AddError(string errorMessage) { SymbolResultTree.AddError(new ParseError(errorMessage, AppliesToPublicSymbolResult)); _conversionResult = ArgumentConversionResult.Failure(this, errorMessage, ArgumentConversionResultType.Failed); diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index d8d0d7e0a3..d0a3aa7043 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -37,10 +37,10 @@ public ParseOperation( _symbolResultTree = new(rootCommand, tokenizationErrors); _innermostCommandResult = _rootCommandResult = new CommandResult( - _configuration.RootCommand, + rootCommand, CurrentToken, _symbolResultTree); - _symbolResultTree.Add(_configuration.RootCommand, _rootCommandResult); + _symbolResultTree.Add(rootCommand, _rootCommandResult); Advance(); } From 27ae77a3a222c7a7d352b00198e2d5de8a0868c5 Mon Sep 17 00:00:00 2001 From: Kevin Bost Date: Wed, 31 Jan 2024 16:19:14 -0800 Subject: [PATCH 011/150] Add System.CommandLine.Extended/Tests project --- System.CommandLine.sln | 30 +++++++++++ .../Directory.Build.props | 10 ++++ .../Program.cs | 10 ++++ .../System.CommandLine.Extended.Tests.csproj | 43 ++++++++++++++++ .../System.CommandLine.Extended.csproj | 51 +++++++++++++++++++ 5 files changed, 144 insertions(+) create mode 100644 src/System.CommandLine.Extended.Tests/Directory.Build.props create mode 100644 src/System.CommandLine.Extended.Tests/Program.cs create mode 100644 src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj create mode 100644 src/System.CommandLine.Extended/System.CommandLine.Extended.csproj diff --git a/System.CommandLine.sln b/System.CommandLine.sln index 2b4452b1d5..8bd9041828 100644 --- a/System.CommandLine.sln +++ b/System.CommandLine.sln @@ -61,6 +61,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.NamingCo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.ApiCompatibility.Tests", "src\System.CommandLine.ApiCompatibility.Tests\System.CommandLine.ApiCompatibility.Tests.csproj", "{A54EE328-D456-4BAF-A180-84E77E6409AC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.CommandLine.Extended", "src\System.CommandLine.Extended\System.CommandLine.Extended.csproj", "{D77D8EE4-7FBA-425C-AEE6-D6908998E228}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.CommandLine.Extended.Tests", "src\System.CommandLine.Extended.Tests\System.CommandLine.Extended.Tests.csproj", "{9E93F66A-6099-4675-AF53-FC10DE01925B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -275,6 +279,30 @@ Global {A54EE328-D456-4BAF-A180-84E77E6409AC}.Release|x64.Build.0 = Release|Any CPU {A54EE328-D456-4BAF-A180-84E77E6409AC}.Release|x86.ActiveCfg = Release|Any CPU {A54EE328-D456-4BAF-A180-84E77E6409AC}.Release|x86.Build.0 = Release|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Debug|x64.ActiveCfg = Debug|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Debug|x64.Build.0 = Debug|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Debug|x86.ActiveCfg = Debug|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Debug|x86.Build.0 = Debug|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Release|Any CPU.Build.0 = Release|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Release|x64.ActiveCfg = Release|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Release|x64.Build.0 = Release|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Release|x86.ActiveCfg = Release|Any CPU + {D77D8EE4-7FBA-425C-AEE6-D6908998E228}.Release|x86.Build.0 = Release|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Debug|x64.ActiveCfg = Debug|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Debug|x64.Build.0 = Debug|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Debug|x86.ActiveCfg = Debug|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Debug|x86.Build.0 = Debug|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Release|Any CPU.Build.0 = Release|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Release|x64.ActiveCfg = Release|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Release|x64.Build.0 = Release|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Release|x86.ActiveCfg = Release|Any CPU + {9E93F66A-6099-4675-AF53-FC10DE01925B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -297,6 +325,8 @@ Global {10DFE204-B027-49DA-BD77-08ECA18DD357} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {789A05F2-5EF6-4FE8-9609-4706207E047E} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {A54EE328-D456-4BAF-A180-84E77E6409AC} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} + {D77D8EE4-7FBA-425C-AEE6-D6908998E228} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} + {9E93F66A-6099-4675-AF53-FC10DE01925B} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5C159F93-800B-49E7-9905-EE09F8B8434A} diff --git a/src/System.CommandLine.Extended.Tests/Directory.Build.props b/src/System.CommandLine.Extended.Tests/Directory.Build.props new file mode 100644 index 0000000000..431713f682 --- /dev/null +++ b/src/System.CommandLine.Extended.Tests/Directory.Build.props @@ -0,0 +1,10 @@ + + + + true + IDE1006 + + + + + diff --git a/src/System.CommandLine.Extended.Tests/Program.cs b/src/System.CommandLine.Extended.Tests/Program.cs new file mode 100644 index 0000000000..a5e09da17e --- /dev/null +++ b/src/System.CommandLine.Extended.Tests/Program.cs @@ -0,0 +1,10 @@ +// 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.Extended.Tests; + +public class Program +{ + public static void Main() + { } +} \ No newline at end of file diff --git a/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj b/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj new file mode 100644 index 0000000000..1b2ebce8a8 --- /dev/null +++ b/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj @@ -0,0 +1,43 @@ + + + + $(TargetFrameworkForNETSDK);net462 + false + $(DefaultExcludesInProjectFolder);TestApps\** + Library + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj b/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj new file mode 100644 index 0000000000..82d160a3ae --- /dev/null +++ b/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj @@ -0,0 +1,51 @@ + + + + true + System.CommandLine.Extended + $(TargetFrameworkForNETSDK);netstandard2.0 + enable + true + latest + Support for parsing command lines, supporting both POSIX and Windows conventions and shell-agnostic command line completions. + true + + + + true + true + true + + + + portable + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + From f19f6ea891d3ea652558b9be1eb065544735b7df Mon Sep 17 00:00:00 2001 From: Jean Joeris Date: Wed, 31 Jan 2024 19:48:20 -0500 Subject: [PATCH 012/150] Move Help to Extended --- .../Help/HelpBuilder.Default.cs | 0 .../Help/HelpBuilder.cs | 0 .../Help/HelpBuilderExtensions.cs | 0 .../Help/HelpContext.cs | 0 .../Help/HelpOption.cs | 0 .../Help/HelpOptionAction.cs | 0 .../Help/TwoColumnHelpRow.cs | 0 .../System.CommandLine.Extended.csproj | 4 ++++ src/System.CommandLine/CliRootCommand.cs | 1 - .../Invocation/ParseErrorAction.cs | 15 ++++++++------- src/System.CommandLine/Parsing/ParseOperation.cs | 1 - 11 files changed, 12 insertions(+), 9 deletions(-) rename src/{System.CommandLine => System.CommandLine.Extended}/Help/HelpBuilder.Default.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Help/HelpBuilder.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Help/HelpBuilderExtensions.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Help/HelpContext.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Help/HelpOption.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Help/HelpOptionAction.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Help/TwoColumnHelpRow.cs (100%) diff --git a/src/System.CommandLine/Help/HelpBuilder.Default.cs b/src/System.CommandLine.Extended/Help/HelpBuilder.Default.cs similarity index 100% rename from src/System.CommandLine/Help/HelpBuilder.Default.cs rename to src/System.CommandLine.Extended/Help/HelpBuilder.Default.cs diff --git a/src/System.CommandLine/Help/HelpBuilder.cs b/src/System.CommandLine.Extended/Help/HelpBuilder.cs similarity index 100% rename from src/System.CommandLine/Help/HelpBuilder.cs rename to src/System.CommandLine.Extended/Help/HelpBuilder.cs diff --git a/src/System.CommandLine/Help/HelpBuilderExtensions.cs b/src/System.CommandLine.Extended/Help/HelpBuilderExtensions.cs similarity index 100% rename from src/System.CommandLine/Help/HelpBuilderExtensions.cs rename to src/System.CommandLine.Extended/Help/HelpBuilderExtensions.cs diff --git a/src/System.CommandLine/Help/HelpContext.cs b/src/System.CommandLine.Extended/Help/HelpContext.cs similarity index 100% rename from src/System.CommandLine/Help/HelpContext.cs rename to src/System.CommandLine.Extended/Help/HelpContext.cs diff --git a/src/System.CommandLine/Help/HelpOption.cs b/src/System.CommandLine.Extended/Help/HelpOption.cs similarity index 100% rename from src/System.CommandLine/Help/HelpOption.cs rename to src/System.CommandLine.Extended/Help/HelpOption.cs diff --git a/src/System.CommandLine/Help/HelpOptionAction.cs b/src/System.CommandLine.Extended/Help/HelpOptionAction.cs similarity index 100% rename from src/System.CommandLine/Help/HelpOptionAction.cs rename to src/System.CommandLine.Extended/Help/HelpOptionAction.cs diff --git a/src/System.CommandLine/Help/TwoColumnHelpRow.cs b/src/System.CommandLine.Extended/Help/TwoColumnHelpRow.cs similarity index 100% rename from src/System.CommandLine/Help/TwoColumnHelpRow.cs rename to src/System.CommandLine.Extended/Help/TwoColumnHelpRow.cs diff --git a/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj b/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj index 82d160a3ae..1f67395bf3 100644 --- a/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj +++ b/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj @@ -48,4 +48,8 @@ + + + + diff --git a/src/System.CommandLine/CliRootCommand.cs b/src/System.CommandLine/CliRootCommand.cs index 352ef5bedb..37b57db5ca 100644 --- a/src/System.CommandLine/CliRootCommand.cs +++ b/src/System.CommandLine/CliRootCommand.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; //using System.CommandLine.Completions; -//using System.CommandLine.Help; using System.IO; using System.Reflection; diff --git a/src/System.CommandLine/Invocation/ParseErrorAction.cs b/src/System.CommandLine/Invocation/ParseErrorAction.cs index b1af669607..17f149749c 100644 --- a/src/System.CommandLine/Invocation/ParseErrorAction.cs +++ b/src/System.CommandLine/Invocation/ParseErrorAction.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.CommandLine.Help; using System.CommandLine.Parsing; using System.Linq; using System.Threading; @@ -36,11 +35,13 @@ public override int Invoke(ParseResult parseResult) WriteErrorDetails(parseResult); + // TODO: move with invocation + /* if (ShowHelp) { WriteHelp(parseResult); } - + */ return 1; } @@ -59,6 +60,8 @@ private static void WriteErrorDetails(ParseResult parseResult) ConsoleHelpers.ResetTerminalForegroundColor(); } + // TODO: move with invocation + /* private static void WriteHelp(ParseResult parseResult) { // Find the most proximate help option (if any) and invoke its action. @@ -66,23 +69,21 @@ private static void WriteHelp(ParseResult parseResult) parseResult .CommandResult .RecurseWhileNotNull(r => r.Parent as CommandResult) - .Select(r => r.Command.Options.OfType().FirstOrDefault()); - + .Select(r => r.Command.Options.OfType().FirstOrDefault()); if (availableHelpOptions.FirstOrDefault(o => o is not null) is { Action: not null } helpOption) { switch (helpOption.Action) { case SynchronousCliAction syncAction: syncAction.Invoke(parseResult); - break; - + break; case AsynchronousCliAction asyncAction: asyncAction.InvokeAsync(parseResult, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); break; } } } - +*/ private static void WriteTypoCorrectionSuggestions(ParseResult parseResult) { var unmatchedTokens = parseResult.UnmatchedTokens; diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index d0a3aa7043..3c6c0ec748 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -//using System.CommandLine.Help; //using System.CommandLine.Invocation; namespace System.CommandLine.Parsing From f045346438db64d556952f12423c6d1aab3dd8cd Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Wed, 31 Jan 2024 17:01:06 -0800 Subject: [PATCH 013/150] Move Completions to Extended --- .../CompletionSourceExtensions.cs | 0 .../Completions/CompletionAction.cs | 0 .../Completions/CompletionContext.cs | 0 .../Completions/CompletionItem.cs | 0 .../Completions/SuggestDirective.cs | 0 .../Completions/TextCompletionContext.cs | 0 src/System.CommandLine/Binding/ArgumentConversionResult.cs | 1 - src/System.CommandLine/CliArgument.cs | 2 -- src/System.CommandLine/CliCommand.cs | 1 - src/System.CommandLine/CliDirective.cs | 1 - src/System.CommandLine/CliOption.cs | 1 - src/System.CommandLine/CliRootCommand.cs | 1 - src/System.CommandLine/CliSymbol.cs | 1 - src/System.CommandLine/Invocation/ParseErrorAction.cs | 1 - src/System.CommandLine/ParseResult.cs | 1 - 15 files changed, 10 deletions(-) rename src/{System.CommandLine => System.CommandLine.Extended}/CompletionSourceExtensions.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Completions/CompletionAction.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Completions/CompletionContext.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Completions/CompletionItem.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Completions/SuggestDirective.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Completions/TextCompletionContext.cs (100%) diff --git a/src/System.CommandLine/CompletionSourceExtensions.cs b/src/System.CommandLine.Extended/CompletionSourceExtensions.cs similarity index 100% rename from src/System.CommandLine/CompletionSourceExtensions.cs rename to src/System.CommandLine.Extended/CompletionSourceExtensions.cs diff --git a/src/System.CommandLine/Completions/CompletionAction.cs b/src/System.CommandLine.Extended/Completions/CompletionAction.cs similarity index 100% rename from src/System.CommandLine/Completions/CompletionAction.cs rename to src/System.CommandLine.Extended/Completions/CompletionAction.cs diff --git a/src/System.CommandLine/Completions/CompletionContext.cs b/src/System.CommandLine.Extended/Completions/CompletionContext.cs similarity index 100% rename from src/System.CommandLine/Completions/CompletionContext.cs rename to src/System.CommandLine.Extended/Completions/CompletionContext.cs diff --git a/src/System.CommandLine/Completions/CompletionItem.cs b/src/System.CommandLine.Extended/Completions/CompletionItem.cs similarity index 100% rename from src/System.CommandLine/Completions/CompletionItem.cs rename to src/System.CommandLine.Extended/Completions/CompletionItem.cs diff --git a/src/System.CommandLine/Completions/SuggestDirective.cs b/src/System.CommandLine.Extended/Completions/SuggestDirective.cs similarity index 100% rename from src/System.CommandLine/Completions/SuggestDirective.cs rename to src/System.CommandLine.Extended/Completions/SuggestDirective.cs diff --git a/src/System.CommandLine/Completions/TextCompletionContext.cs b/src/System.CommandLine.Extended/Completions/TextCompletionContext.cs similarity index 100% rename from src/System.CommandLine/Completions/TextCompletionContext.cs rename to src/System.CommandLine.Extended/Completions/TextCompletionContext.cs diff --git a/src/System.CommandLine/Binding/ArgumentConversionResult.cs b/src/System.CommandLine/Binding/ArgumentConversionResult.cs index 6a22e6e98f..8c151c6d5d 100644 --- a/src/System.CommandLine/Binding/ArgumentConversionResult.cs +++ b/src/System.CommandLine/Binding/ArgumentConversionResult.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.CommandLine.Completions; using System.CommandLine.Parsing; using System.Linq; diff --git a/src/System.CommandLine/CliArgument.cs b/src/System.CommandLine/CliArgument.cs index a16365b1b5..617f1d5b9b 100644 --- a/src/System.CommandLine/CliArgument.cs +++ b/src/System.CommandLine/CliArgument.cs @@ -4,8 +4,6 @@ using System.Collections.Generic; using System.CommandLine.Binding; using System.CommandLine.Parsing; -//using System.CommandLine.Completions; -using System.Linq; namespace System.CommandLine { diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index 4d7fb61faf..b1ffde79ad 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; -//using System.CommandLine.Completions; //using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.ComponentModel; diff --git a/src/System.CommandLine/CliDirective.cs b/src/System.CommandLine/CliDirective.cs index 61bfdbdaaf..91c8c66907 100644 --- a/src/System.CommandLine/CliDirective.cs +++ b/src/System.CommandLine/CliDirective.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -//using System.CommandLine.Completions; //using System.CommandLine.Invocation; namespace System.CommandLine diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index da146d256e..027d8909d2 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -//using System.CommandLine.Completions; //using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Linq; diff --git a/src/System.CommandLine/CliRootCommand.cs b/src/System.CommandLine/CliRootCommand.cs index 37b57db5ca..a5c472a4a3 100644 --- a/src/System.CommandLine/CliRootCommand.cs +++ b/src/System.CommandLine/CliRootCommand.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -//using System.CommandLine.Completions; using System.IO; using System.Reflection; diff --git a/src/System.CommandLine/CliSymbol.cs b/src/System.CommandLine/CliSymbol.cs index ecdf9fcd1b..b120efd534 100644 --- a/src/System.CommandLine/CliSymbol.cs +++ b/src/System.CommandLine/CliSymbol.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -//using System.CommandLine.Completions; using System.Diagnostics; namespace System.CommandLine diff --git a/src/System.CommandLine/Invocation/ParseErrorAction.cs b/src/System.CommandLine/Invocation/ParseErrorAction.cs index 17f149749c..5175ad70d7 100644 --- a/src/System.CommandLine/Invocation/ParseErrorAction.cs +++ b/src/System.CommandLine/Invocation/ParseErrorAction.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.CommandLine.Parsing; using System.Linq; using System.Threading; diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 43590aedb2..c99d59fe32 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -//using System.CommandLine.Completions; //using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Linq; From 79054e64a6401f1d6c443207b20d488ec3b9bcfd Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Wed, 31 Jan 2024 17:11:59 -0800 Subject: [PATCH 014/150] Move Invocations to Extended --- .../Invocation/AnonymousAsynchronousCliAction.cs | 0 .../Invocation/AnonymousSynchronousCliAction.cs | 0 .../Invocation/AsynchronousCliAction.cs | 0 .../Invocation/CliAction.cs | 0 .../Invocation/InvocationPipeline.cs | 0 .../Invocation/ParseErrorAction.cs | 0 .../Invocation/ProcessTerminationHandler.cs | 0 .../Invocation/SynchronousCliAction.cs | 0 src/System.CommandLine/CliCommand.cs | 3 +-- src/System.CommandLine/CliConfiguration.cs | 1 - src/System.CommandLine/CliDirective.cs | 1 - src/System.CommandLine/CliOption.cs | 1 - src/System.CommandLine/ParseResult.cs | 1 - src/System.CommandLine/Parsing/ParseOperation.cs | 3 +-- 14 files changed, 2 insertions(+), 8 deletions(-) rename src/{System.CommandLine => System.CommandLine.Extended}/Invocation/AnonymousAsynchronousCliAction.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Invocation/AnonymousSynchronousCliAction.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Invocation/AsynchronousCliAction.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Invocation/CliAction.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Invocation/InvocationPipeline.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Invocation/ParseErrorAction.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Invocation/ProcessTerminationHandler.cs (100%) rename src/{System.CommandLine => System.CommandLine.Extended}/Invocation/SynchronousCliAction.cs (100%) diff --git a/src/System.CommandLine/Invocation/AnonymousAsynchronousCliAction.cs b/src/System.CommandLine.Extended/Invocation/AnonymousAsynchronousCliAction.cs similarity index 100% rename from src/System.CommandLine/Invocation/AnonymousAsynchronousCliAction.cs rename to src/System.CommandLine.Extended/Invocation/AnonymousAsynchronousCliAction.cs diff --git a/src/System.CommandLine/Invocation/AnonymousSynchronousCliAction.cs b/src/System.CommandLine.Extended/Invocation/AnonymousSynchronousCliAction.cs similarity index 100% rename from src/System.CommandLine/Invocation/AnonymousSynchronousCliAction.cs rename to src/System.CommandLine.Extended/Invocation/AnonymousSynchronousCliAction.cs diff --git a/src/System.CommandLine/Invocation/AsynchronousCliAction.cs b/src/System.CommandLine.Extended/Invocation/AsynchronousCliAction.cs similarity index 100% rename from src/System.CommandLine/Invocation/AsynchronousCliAction.cs rename to src/System.CommandLine.Extended/Invocation/AsynchronousCliAction.cs diff --git a/src/System.CommandLine/Invocation/CliAction.cs b/src/System.CommandLine.Extended/Invocation/CliAction.cs similarity index 100% rename from src/System.CommandLine/Invocation/CliAction.cs rename to src/System.CommandLine.Extended/Invocation/CliAction.cs diff --git a/src/System.CommandLine/Invocation/InvocationPipeline.cs b/src/System.CommandLine.Extended/Invocation/InvocationPipeline.cs similarity index 100% rename from src/System.CommandLine/Invocation/InvocationPipeline.cs rename to src/System.CommandLine.Extended/Invocation/InvocationPipeline.cs diff --git a/src/System.CommandLine/Invocation/ParseErrorAction.cs b/src/System.CommandLine.Extended/Invocation/ParseErrorAction.cs similarity index 100% rename from src/System.CommandLine/Invocation/ParseErrorAction.cs rename to src/System.CommandLine.Extended/Invocation/ParseErrorAction.cs diff --git a/src/System.CommandLine/Invocation/ProcessTerminationHandler.cs b/src/System.CommandLine.Extended/Invocation/ProcessTerminationHandler.cs similarity index 100% rename from src/System.CommandLine/Invocation/ProcessTerminationHandler.cs rename to src/System.CommandLine.Extended/Invocation/ProcessTerminationHandler.cs diff --git a/src/System.CommandLine/Invocation/SynchronousCliAction.cs b/src/System.CommandLine.Extended/Invocation/SynchronousCliAction.cs similarity index 100% rename from src/System.CommandLine/Invocation/SynchronousCliAction.cs rename to src/System.CommandLine.Extended/Invocation/SynchronousCliAction.cs diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index b1ffde79ad..fc3c88801a 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; -//using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.ComponentModel; using System.Diagnostics; @@ -135,7 +134,7 @@ public void SetAction(Func action) throw new ArgumentNullException(nameof(action)); } - Action = new AnonymousSynchronousCliAction(action); + Action = new AnonymousSynchronousCliAction(action); } /// diff --git a/src/System.CommandLine/CliConfiguration.cs b/src/System.CommandLine/CliConfiguration.cs index c8461b7e24..cd63a01c20 100644 --- a/src/System.CommandLine/CliConfiguration.cs +++ b/src/System.CommandLine/CliConfiguration.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using System.Threading; using System.IO; -//using System.CommandLine.Invocation; namespace System.CommandLine { diff --git a/src/System.CommandLine/CliDirective.cs b/src/System.CommandLine/CliDirective.cs index 91c8c66907..406bdebc40 100644 --- a/src/System.CommandLine/CliDirective.cs +++ b/src/System.CommandLine/CliDirective.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -//using System.CommandLine.Invocation; namespace System.CommandLine { diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index 027d8909d2..cdf9767036 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -//using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Linq; diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index c99d59fe32..35f940e6c1 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -//using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Linq; using System.Threading.Tasks; diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 3c6c0ec748..1bbf6fb0c1 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -//using System.CommandLine.Invocation; namespace System.CommandLine.Parsing { @@ -78,7 +77,7 @@ internal ParseResult Parse() } */ - return new ( + return new( _configuration, _rootCommandResult, _innermostCommandResult, From 50bdf1f3d62b2b8363d5b3c5d89da0b6516b8556 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Wed, 31 Jan 2024 17:22:01 -0800 Subject: [PATCH 015/150] Move out Help, Completions and Invocation Tests to Extended --- .../CompilationTests.cs | 0 .../CompletionContextTests.cs | 0 .../CompletionTests.cs | 0 .../Help/ApprovalTests.Config.cs | 0 .../HelpBuilderTests.Help_layout_has_not_changed.approved.txt | 0 .../Help/HelpBuilderExtensions.cs | 0 .../Help/HelpBuilderTests.Approval.cs | 0 .../Help/HelpBuilderTests.Customization.cs | 0 .../Help/HelpBuilderTests.cs | 0 .../HelpOptionTests.cs | 0 .../Invocation/CancelOnProcessTerminationTests.cs | 0 .../Invocation/InvocationTests.cs | 0 .../Invocation/TypoCorrectionTests.cs | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/CompilationTests.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/CompletionContextTests.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/CompletionTests.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/Help/ApprovalTests.Config.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/Help/HelpBuilderExtensions.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/Help/HelpBuilderTests.Approval.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/Help/HelpBuilderTests.Customization.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/Help/HelpBuilderTests.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/HelpOptionTests.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/Invocation/CancelOnProcessTerminationTests.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/Invocation/InvocationTests.cs (100%) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/Invocation/TypoCorrectionTests.cs (100%) diff --git a/src/System.CommandLine.Tests/CompilationTests.cs b/src/System.CommandLine.Extended.Tests/CompilationTests.cs similarity index 100% rename from src/System.CommandLine.Tests/CompilationTests.cs rename to src/System.CommandLine.Extended.Tests/CompilationTests.cs diff --git a/src/System.CommandLine.Tests/CompletionContextTests.cs b/src/System.CommandLine.Extended.Tests/CompletionContextTests.cs similarity index 100% rename from src/System.CommandLine.Tests/CompletionContextTests.cs rename to src/System.CommandLine.Extended.Tests/CompletionContextTests.cs diff --git a/src/System.CommandLine.Tests/CompletionTests.cs b/src/System.CommandLine.Extended.Tests/CompletionTests.cs similarity index 100% rename from src/System.CommandLine.Tests/CompletionTests.cs rename to src/System.CommandLine.Extended.Tests/CompletionTests.cs diff --git a/src/System.CommandLine.Tests/Help/ApprovalTests.Config.cs b/src/System.CommandLine.Extended.Tests/Help/ApprovalTests.Config.cs similarity index 100% rename from src/System.CommandLine.Tests/Help/ApprovalTests.Config.cs rename to src/System.CommandLine.Extended.Tests/Help/ApprovalTests.Config.cs diff --git a/src/System.CommandLine.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt b/src/System.CommandLine.Extended.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt similarity index 100% rename from src/System.CommandLine.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt rename to src/System.CommandLine.Extended.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderExtensions.cs b/src/System.CommandLine.Extended.Tests/Help/HelpBuilderExtensions.cs similarity index 100% rename from src/System.CommandLine.Tests/Help/HelpBuilderExtensions.cs rename to src/System.CommandLine.Extended.Tests/Help/HelpBuilderExtensions.cs diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Approval.cs b/src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.Approval.cs similarity index 100% rename from src/System.CommandLine.Tests/Help/HelpBuilderTests.Approval.cs rename to src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.Approval.cs diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs b/src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.Customization.cs similarity index 100% rename from src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs rename to src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.Customization.cs diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs b/src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.cs similarity index 100% rename from src/System.CommandLine.Tests/Help/HelpBuilderTests.cs rename to src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.cs diff --git a/src/System.CommandLine.Tests/HelpOptionTests.cs b/src/System.CommandLine.Extended.Tests/HelpOptionTests.cs similarity index 100% rename from src/System.CommandLine.Tests/HelpOptionTests.cs rename to src/System.CommandLine.Extended.Tests/HelpOptionTests.cs diff --git a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs b/src/System.CommandLine.Extended.Tests/Invocation/CancelOnProcessTerminationTests.cs similarity index 100% rename from src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs rename to src/System.CommandLine.Extended.Tests/Invocation/CancelOnProcessTerminationTests.cs diff --git a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs b/src/System.CommandLine.Extended.Tests/Invocation/InvocationTests.cs similarity index 100% rename from src/System.CommandLine.Tests/Invocation/InvocationTests.cs rename to src/System.CommandLine.Extended.Tests/Invocation/InvocationTests.cs diff --git a/src/System.CommandLine.Tests/Invocation/TypoCorrectionTests.cs b/src/System.CommandLine.Extended.Tests/Invocation/TypoCorrectionTests.cs similarity index 100% rename from src/System.CommandLine.Tests/Invocation/TypoCorrectionTests.cs rename to src/System.CommandLine.Extended.Tests/Invocation/TypoCorrectionTests.cs From 690a19a2f0f7d5776da507656e117be5d202e3b8 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sun, 18 Feb 2024 14:41:43 -0500 Subject: [PATCH 016/150] Get Extended/Tests building Exclude all files for now and we can bring them back in as we make them compilable, as we have been doing on System.CommandLine/Tests --- .../System.CommandLine.Extended.Tests.csproj | 7 ++++++- src/System.CommandLine.Extended/Help/HelpOption.cs | 3 ++- .../System.CommandLine.Extended.csproj | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj b/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj index 1b2ebce8a8..f194d83ce7 100644 --- a/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj +++ b/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj @@ -5,6 +5,7 @@ false $(DefaultExcludesInProjectFolder);TestApps\** Library + False @@ -13,15 +14,19 @@ + + + + diff --git a/src/System.CommandLine.Extended/Help/HelpOption.cs b/src/System.CommandLine.Extended/Help/HelpOption.cs index c4c53f194c..989fe9ea3f 100644 --- a/src/System.CommandLine.Extended/Help/HelpOption.cs +++ b/src/System.CommandLine.Extended/Help/HelpOption.cs @@ -30,8 +30,9 @@ public HelpOption() : this("--help", new[] { "-h", "/h", "-?", "/?" }) /// When added to a , it configures the application to show help when given name or one of the aliases are specified on the command line. /// public HelpOption(string name, params string[] aliases) - : base(name, aliases, new CliArgument(name) { Arity = ArgumentArity.Zero }) + : base(name, aliases) { + Arity = ArgumentArity.Zero; Recursive = true; Description = LocalizationResources.HelpOptionDescription(); } diff --git a/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj b/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj index 1f67395bf3..1a858da874 100644 --- a/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj +++ b/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj @@ -9,6 +9,7 @@ latest Support for parsing command lines, supporting both POSIX and Windows conventions and shell-agnostic command line completions. true + False @@ -23,6 +24,7 @@ + From 1b845305db00e1e436927df5b4799cb3b2084a4c Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sun, 18 Feb 2024 14:49:39 -0500 Subject: [PATCH 017/150] Move VersionOption to Extended --- .../System.CommandLine.Extended.csproj | 1 + .../VersionOption.cs | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) rename src/{System.CommandLine => System.CommandLine.Extended}/VersionOption.cs (92%) diff --git a/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj b/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj index 1a858da874..d52c79f48f 100644 --- a/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj +++ b/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj @@ -24,6 +24,7 @@ + diff --git a/src/System.CommandLine/VersionOption.cs b/src/System.CommandLine.Extended/VersionOption.cs similarity index 92% rename from src/System.CommandLine/VersionOption.cs rename to src/System.CommandLine.Extended/VersionOption.cs index 3186262469..48f56dbc77 100644 --- a/src/System.CommandLine/VersionOption.cs +++ b/src/System.CommandLine.Extended/VersionOption.cs @@ -1,7 +1,7 @@ // 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.Invocation; +//using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Linq; @@ -12,8 +12,10 @@ namespace System.CommandLine /// public sealed class VersionOption : CliOption { +// TODO: invocation +/* private CliAction? _action; - +*/ /// /// When added to a , it enables the use of a --version option, which when specified in command line input will short circuit normal command handling and instead write out version information before exiting. /// @@ -25,8 +27,11 @@ public VersionOption() : this("--version", Array.Empty()) /// When added to a , it enables the use of a provided option name and aliases, which when specified in command line input will short circuit normal command handling and instead write out version information before exiting. /// public VersionOption(string name, params string[] aliases) - : base(name, aliases, new CliArgument("--version") { Arity = ArgumentArity.Zero }) + : base(name, aliases) { + Arity = ArgumentArity.Zero; +// TODO: help, validators, invocation, access to IsGreedy +/* Description = LocalizationResources.VersionOptionDescription(); AddValidators(); } @@ -59,6 +64,7 @@ public override int Invoke(ParseResult parseResult) parseResult.Configuration.Output.WriteLine(CliRootCommand.ExecutableVersion); return 0; } +*/ } } } \ No newline at end of file From 695edba7ec9887261e6644ba697fdf25b01a8e75 Mon Sep 17 00:00:00 2001 From: Kevin Bost Date: Sat, 17 Feb 2024 13:17:20 -0500 Subject: [PATCH 018/150] Version options tests --- .../VersionOptionTests.cs | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) rename src/{System.CommandLine.Tests => System.CommandLine.Extended.Tests}/VersionOptionTests.cs (77%) diff --git a/src/System.CommandLine.Tests/VersionOptionTests.cs b/src/System.CommandLine.Extended.Tests/VersionOptionTests.cs similarity index 77% rename from src/System.CommandLine.Tests/VersionOptionTests.cs rename to src/System.CommandLine.Extended.Tests/VersionOptionTests.cs index e331d72261..a3bd031dc6 100644 --- a/src/System.CommandLine.Tests/VersionOptionTests.cs +++ b/src/System.CommandLine.Extended.Tests/VersionOptionTests.cs @@ -1,22 +1,19 @@ // 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.Help; using System.IO; -using System.Linq; using System.Reflection; using System.Threading.Tasks; using FluentAssertions; using Xunit; -using static System.Environment; -namespace System.CommandLine.Tests +namespace System.CommandLine.Extended.Tests { public class VersionOptionTests { private static readonly string version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) - .GetCustomAttribute() - .InformationalVersion; + .GetCustomAttribute() + .InformationalVersion; [Fact] public async Task When_the_version_option_is_specified_then_the_version_is_written_to_standard_out() @@ -48,6 +45,17 @@ public async Task When_the_version_option_is_specified_then_invocation_is_short_ wasCalled.Should().BeFalse(); } + [Fact] + public void When_the_version_option_is_specified_then_the_version_is_parsed() + { + ParseResult parseResult = CliParser.Parse ( + new CliRootCommand(), + [ "--version"]); + + parseResult.Errors.Should().BeEmpty(); + parseResult.GetValue(configuration.RootCommand.Options.OfType().Single()).Should().BeTrue(); + } + [Fact] public async Task Version_option_appears_in_help() { @@ -89,7 +97,7 @@ public async Task When_the_version_option_is_specified_and_there_are_default_opt [Fact] public async Task When_the_version_option_is_specified_and_there_are_default_arguments_then_the_version_is_written_to_standard_out() { - CliRootCommand rootCommand = new () + CliRootCommand rootCommand = new() { new CliArgument("x") { DefaultValueFactory =(_) => true }, }; @@ -111,13 +119,11 @@ public async Task When_the_version_option_is_specified_and_there_are_default_arg public void Version_is_not_valid_with_other_tokens(string commandLine) { var subcommand = new CliCommand("subcommand"); - subcommand.SetAction(_ => { }); var rootCommand = new CliRootCommand { subcommand, new CliOption("-x") }; - rootCommand.SetAction(_ => { }); CliConfiguration configuration = new(rootCommand) { @@ -133,13 +139,11 @@ public void Version_is_not_valid_with_other_tokens(string commandLine) public void Version_option_is_not_added_to_subcommands() { var childCommand = new CliCommand("subcommand"); - childCommand.SetAction(_ => { }); var rootCommand = new CliRootCommand { childCommand }; - rootCommand.SetAction(_ => { }); CliConfiguration configuration = new(rootCommand) { @@ -147,51 +151,45 @@ public void Version_option_is_not_added_to_subcommands() }; configuration - .RootCommand - .Subcommands - .Single(c => c.Name == "subcommand") - .Options - .Should() - .BeEmpty(); + .RootCommand + .Subcommands + .Single(c => c.Name == "subcommand") + .Options + .Should() + .BeEmpty(); } [Fact] - public async Task Version_can_specify_additional_alias() + public void Version_can_specify_additional_alias() { CliRootCommand rootCommand = new(); + VersionOption versionOption = new("-version", "-v"); for (int i = 0; i < rootCommand.Options.Count; i++) { if (rootCommand.Options[i] is VersionOption) - rootCommand.Options[i] = new VersionOption("-v", "-version"); + rootCommand.Options[i] = versionOption; } - CliConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; - - await configuration.InvokeAsync("-v"); - configuration.Output.ToString().Should().Be($"{version}{NewLine}"); + var parseResult = rootCommand.Parse("-version"); + var versionSpecified = parseResult.GetValue(versionOption); + versionSpecified.Should().BeTrue(); - configuration.Output = new StringWriter(); - await configuration.InvokeAsync("-version"); - configuration.Output.ToString().Should().Be($"{version}{NewLine}"); + parseResult = rootCommand.Parse("-v"); + versionSpecified = parseResult.GetValue(versionOption); + versionSpecified.Should().BeTrue(); } [Fact] public void Version_is_not_valid_with_other_tokens_uses_custom_alias() { var childCommand = new CliCommand("subcommand"); - childCommand.SetAction((_) => { }); var rootCommand = new CliRootCommand { childCommand }; - rootCommand.Options[1] = new VersionOption("-v"); - - rootCommand.SetAction((_) => { }); + rootCommand.Options[0] = new VersionOption("-v"); CliConfiguration configuration = new(rootCommand) { @@ -203,4 +201,4 @@ public void Version_is_not_valid_with_other_tokens_uses_custom_alias() result.Errors.Should().ContainSingle(e => e.Message == "-v option cannot be combined with other arguments."); } } -} +} \ No newline at end of file From ae01dde13fe3da6a79981ca672268d9969152a51 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Mon, 19 Feb 2024 00:44:51 -0500 Subject: [PATCH 019/150] Support initializing CliCommand via collection expression Roslyn analyzer was suggesting using a collection expression to initialize CliCommand but the change made by the fix failed to compile as CliCommand implemented IEnumerable but had no overload for Add that accepted object. --- src/System.CommandLine/CliCommand.cs | 40 +++++++++++++++++++++++++--- src/System.CommandLine/CliSymbol.cs | 2 +- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index fc3c88801a..0629ff293e 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -21,13 +21,14 @@ namespace System.CommandLine /// for simple applications that only have one action. For example, dotnet run /// uses run as the command. /// - public class CliCommand : CliSymbol, IEnumerable + public class CliCommand : CliSymbol, IEnumerable { // TODO: don't expose field internal AliasSet? _aliases; private ChildSymbolList? _arguments; private ChildSymbolList? _options; private ChildSymbolList? _subcommands; +// TODO: validators /* private List>? _validators; @@ -41,6 +42,7 @@ public CliCommand(string name)/*, string? description = null) */ : base(name) { } +// TODO: help //=> Description = description; /// @@ -188,8 +190,33 @@ public void SetAction(Func> action) /// The Command to add to the command. public void Add(CliCommand command) => Subcommands.Add(command); - /* + // Hide from IntelliSense as it's only to support initializing via C# collection expression + // More specific efficient overloads are available for all supported symbol types. + [DebuggerStepThrough] + [EditorBrowsable(EditorBrowsableState.Never)] + public void Add(CliSymbol symbol) + { + if (symbol is CliCommand cmd) + { + Add(cmd); + } + else if (symbol is CliOption option) + { + Add(option); + } + else if (symbol is CliCommand command) + { + Add(command); + } + else + { +// TODO: add a localized message here + throw new ArgumentException(null, nameof(symbol)); + } + } +// TODO: umatched tokens + /* /// /// Gets or sets a value that indicates whether unmatched tokens should be treated as errors. For example, /// if set to and an extra command or argument is provided, validation will fail. @@ -197,9 +224,16 @@ public void SetAction(Func> action) public bool TreatUnmatchedTokensAsErrors { get; set; } = true; */ /// + // Hide from IntelliSense as it's only to support C# collection initializer [DebuggerStepThrough] - [EditorBrowsable(EditorBrowsableState.Never)] // hide from intellisense, it's public for C# collection initializer + [EditorBrowsable(EditorBrowsableState.Never)] IEnumerator IEnumerable.GetEnumerator() => Children.GetEnumerator(); + + /// + // Hide from IntelliSense as it's only to support initializing via C# collection expression + [DebuggerStepThrough] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerator IEnumerable.GetEnumerator() => Children.GetEnumerator(); /* /// /// Parses an array strings using the command. diff --git a/src/System.CommandLine/CliSymbol.cs b/src/System.CommandLine/CliSymbol.cs index b120efd534..5ecd7320cd 100644 --- a/src/System.CommandLine/CliSymbol.cs +++ b/src/System.CommandLine/CliSymbol.cs @@ -15,8 +15,8 @@ private protected CliSymbol(string name, bool allowWhitespace = false) { Name = ThrowIfEmptyOrWithWhitespaces(name, nameof(name), allowWhitespace); } + // TODO: help /* - /// /// Gets or sets the description of the symbol. /// From 37120f16a24d76be7df084e0e31287c1c102fc66 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Mon, 19 Feb 2024 00:29:21 -0500 Subject: [PATCH 020/150] Get some VersionOption tests compiling/passing --- .../System.CommandLine.Extended.Tests.csproj | 1 + .../VersionOptionTests.cs | 51 +++++++------------ 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj b/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj index f194d83ce7..5506d5b74a 100644 --- a/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj +++ b/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/src/System.CommandLine.Extended.Tests/VersionOptionTests.cs b/src/System.CommandLine.Extended.Tests/VersionOptionTests.cs index a3bd031dc6..d0d1b28499 100644 --- a/src/System.CommandLine.Extended.Tests/VersionOptionTests.cs +++ b/src/System.CommandLine.Extended.Tests/VersionOptionTests.cs @@ -4,13 +4,17 @@ using System.IO; using System.Reflection; using System.Threading.Tasks; +using System.Linq; using FluentAssertions; using Xunit; +using System.CommandLine.Parsing; namespace System.CommandLine.Extended.Tests { public class VersionOptionTests { +// TODO: invocation/output +/* private static readonly string version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) .GetCustomAttribute() .InformationalVersion; @@ -112,11 +116,14 @@ public async Task When_the_version_option_is_specified_and_there_are_default_arg configuration.Output.ToString().Should().Be($"{version}{NewLine}"); } +*/ + + const string SkipValidationTests = "VersionOption does not yet do validation"; [Theory] - [InlineData("--version -x")] - [InlineData("--version subcommand")] - public void Version_is_not_valid_with_other_tokens(string commandLine) + [InlineData("--version", "-x", Skip = SkipValidationTests)] + [InlineData("--version", "subcommand", Skip = SkipValidationTests)] + public void Version_is_not_valid_with_other_tokens(params string[] commandLine) { var subcommand = new CliCommand("subcommand"); var rootCommand = new CliRootCommand @@ -125,12 +132,7 @@ public void Version_is_not_valid_with_other_tokens(string commandLine) new CliOption("-x") }; - CliConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; - - var result = rootCommand.Parse(commandLine, configuration); + var result = CliParser.Parse(rootCommand, commandLine); result.Errors.Should().Contain(e => e.Message == "--version option cannot be combined with other arguments."); } @@ -145,13 +147,7 @@ public void Version_option_is_not_added_to_subcommands() childCommand }; - CliConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; - - configuration - .RootCommand + rootCommand .Subcommands .Single(c => c.Name == "subcommand") .Options @@ -162,25 +158,19 @@ public void Version_option_is_not_added_to_subcommands() [Fact] public void Version_can_specify_additional_alias() { - CliRootCommand rootCommand = new(); - - VersionOption versionOption = new("-version", "-v"); - for (int i = 0; i < rootCommand.Options.Count; i++) - { - if (rootCommand.Options[i] is VersionOption) - rootCommand.Options[i] = versionOption; - } + var versionOption = new VersionOption("-version", "-v"); + CliRootCommand rootCommand = [versionOption]; - var parseResult = rootCommand.Parse("-version"); + var parseResult = CliParser.Parse(rootCommand, ["-version"]); var versionSpecified = parseResult.GetValue(versionOption); versionSpecified.Should().BeTrue(); - parseResult = rootCommand.Parse("-v"); + parseResult = CliParser.Parse(rootCommand, ["-v"]); versionSpecified = parseResult.GetValue(versionOption); versionSpecified.Should().BeTrue(); } - [Fact] + [Fact(Skip = SkipValidationTests)] public void Version_is_not_valid_with_other_tokens_uses_custom_alias() { var childCommand = new CliCommand("subcommand"); @@ -191,12 +181,7 @@ public void Version_is_not_valid_with_other_tokens_uses_custom_alias() rootCommand.Options[0] = new VersionOption("-v"); - CliConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; - - var result = rootCommand.Parse("-v subcommand", configuration); + var result = CliParser.Parse(rootCommand, ["-v", "subcommand"]); result.Errors.Should().ContainSingle(e => e.Message == "-v option cannot be combined with other arguments."); } From 179bc8850832b34b2cfcc846a8c77939f5d9ff9e Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Tue, 27 Feb 2024 15:11:37 -0500 Subject: [PATCH 021/150] Updated solution to remove unneeded projects. Saving files in repo as some code will be needed. --- System.CommandLine.sln | 186 +----------------- samples/DragonFruit/DragonFruit.csproj | 4 - .../HostingPlayground.csproj | 2 - .../RenderingPlayground.csproj | 5 - .../ApiCompatibilityApprovalTests.cs | 18 -- ....CommandLine.ApiCompatibility.Tests.csproj | 2 - 6 files changed, 2 insertions(+), 215 deletions(-) diff --git a/System.CommandLine.sln b/System.CommandLine.sln index 8bd9041828..23b734a558 100644 --- a/System.CommandLine.sln +++ b/System.CommandLine.sln @@ -29,41 +29,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine", "src\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Tests", "src\System.CommandLine.Tests\System.CommandLine.Tests.csproj", "{F843CCCA-4CC9-422C-A881-3AE6A998B53F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{6749FB3E-39DE-4321-A39E-525278E9408D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DragonFruit", "samples\DragonFruit\DragonFruit.csproj", "{8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.DragonFruit", "src\System.CommandLine.DragonFruit\System.CommandLine.DragonFruit.csproj", "{EEC30462-078F-45EB-AA70-12E3170CD51E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.DragonFruit.Tests", "src\System.CommandLine.DragonFruit.Tests\System.CommandLine.DragonFruit.Tests.csproj", "{1F4B2074-F651-4A02-A860-7DDA74B2CC5F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-suggest", "src\System.CommandLine.Suggest\dotnet-suggest.csproj", "{E23C760E-B826-4B4F-BE76-916D86BAD2DB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-suggest.Tests", "src\System.CommandLine.Suggest.Tests\dotnet-suggest.Tests.csproj", "{E41F0471-B14D-4FA0-9D8B-1E7750695AE9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderingPlayground", "samples\RenderingPlayground\RenderingPlayground.csproj", "{8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Rendering", "src\System.CommandLine.Rendering\System.CommandLine.Rendering.csproj", "{27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Rendering.Tests", "src\System.CommandLine.Rendering.Tests\System.CommandLine.Rendering.Tests.csproj", "{9E574595-A9CD-441A-9328-1D4DD5B531E8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Hosting", "src\System.CommandLine.Hosting\System.CommandLine.Hosting.csproj", "{644C4B4A-4A32-4307-9F71-C3BF901FFB66}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Hosting.Tests", "src\System.CommandLine.Hosting.Tests\System.CommandLine.Hosting.Tests.csproj", "{39483140-BC26-4CAD-BBAE-3DC76C2F16CF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostingPlayground", "samples\HostingPlayground\HostingPlayground.csproj", "{0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Generator.CommandHandler", "src\System.CommandLine.Generator.CommandHandler\System.CommandLine.Generator.CommandHandler.csproj", "{591EF370-7AD7-4624-8B9D-FD15010CA657}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.NamingConventionBinder", "src\System.CommandLine.NamingConventionBinder\System.CommandLine.NamingConventionBinder.csproj", "{10DFE204-B027-49DA-BD77-08ECA18DD357}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.NamingConventionBinder.Tests", "src\System.CommandLine.NamingConventionBinder.Tests\System.CommandLine.NamingConventionBinder.Tests.csproj", "{789A05F2-5EF6-4FE8-9609-4706207E047E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.ApiCompatibility.Tests", "src\System.CommandLine.ApiCompatibility.Tests\System.CommandLine.ApiCompatibility.Tests.csproj", "{A54EE328-D456-4BAF-A180-84E77E6409AC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.CommandLine.Extended", "src\System.CommandLine.Extended\System.CommandLine.Extended.csproj", "{D77D8EE4-7FBA-425C-AEE6-D6908998E228}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Extended", "src\System.CommandLine.Extended\System.CommandLine.Extended.csproj", "{D77D8EE4-7FBA-425C-AEE6-D6908998E228}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.CommandLine.Extended.Tests", "src\System.CommandLine.Extended.Tests\System.CommandLine.Extended.Tests.csproj", "{9E93F66A-6099-4675-AF53-FC10DE01925B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Extended.Tests", "src\System.CommandLine.Extended.Tests\System.CommandLine.Extended.Tests.csproj", "{9E93F66A-6099-4675-AF53-FC10DE01925B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -99,42 +73,6 @@ Global {F843CCCA-4CC9-422C-A881-3AE6A998B53F}.Release|x64.Build.0 = Release|Any CPU {F843CCCA-4CC9-422C-A881-3AE6A998B53F}.Release|x86.ActiveCfg = Release|Any CPU {F843CCCA-4CC9-422C-A881-3AE6A998B53F}.Release|x86.Build.0 = Release|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Debug|x64.ActiveCfg = Debug|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Debug|x64.Build.0 = Debug|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Debug|x86.ActiveCfg = Debug|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Debug|x86.Build.0 = Debug|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Release|Any CPU.Build.0 = Release|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Release|x64.ActiveCfg = Release|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Release|x64.Build.0 = Release|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Release|x86.ActiveCfg = Release|Any CPU - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00}.Release|x86.Build.0 = Release|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Debug|x64.ActiveCfg = Debug|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Debug|x64.Build.0 = Debug|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Debug|x86.ActiveCfg = Debug|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Debug|x86.Build.0 = Debug|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Release|Any CPU.Build.0 = Release|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Release|x64.ActiveCfg = Release|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Release|x64.Build.0 = Release|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Release|x86.ActiveCfg = Release|Any CPU - {EEC30462-078F-45EB-AA70-12E3170CD51E}.Release|x86.Build.0 = Release|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Debug|x64.ActiveCfg = Debug|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Debug|x64.Build.0 = Debug|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Debug|x86.ActiveCfg = Debug|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Debug|x86.Build.0 = Debug|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Release|Any CPU.Build.0 = Release|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Release|x64.ActiveCfg = Release|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Release|x64.Build.0 = Release|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Release|x86.ActiveCfg = Release|Any CPU - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F}.Release|x86.Build.0 = Release|Any CPU {E23C760E-B826-4B4F-BE76-916D86BAD2DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E23C760E-B826-4B4F-BE76-916D86BAD2DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {E23C760E-B826-4B4F-BE76-916D86BAD2DB}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -159,114 +97,6 @@ Global {E41F0471-B14D-4FA0-9D8B-1E7750695AE9}.Release|x64.Build.0 = Release|Any CPU {E41F0471-B14D-4FA0-9D8B-1E7750695AE9}.Release|x86.ActiveCfg = Release|Any CPU {E41F0471-B14D-4FA0-9D8B-1E7750695AE9}.Release|x86.Build.0 = Release|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Debug|x64.ActiveCfg = Debug|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Debug|x64.Build.0 = Debug|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Debug|x86.ActiveCfg = Debug|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Debug|x86.Build.0 = Debug|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Release|Any CPU.Build.0 = Release|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Release|x64.ActiveCfg = Release|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Release|x64.Build.0 = Release|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Release|x86.ActiveCfg = Release|Any CPU - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E}.Release|x86.Build.0 = Release|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Debug|x64.ActiveCfg = Debug|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Debug|x64.Build.0 = Debug|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Debug|x86.ActiveCfg = Debug|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Debug|x86.Build.0 = Debug|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Release|Any CPU.Build.0 = Release|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Release|x64.ActiveCfg = Release|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Release|x64.Build.0 = Release|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Release|x86.ActiveCfg = Release|Any CPU - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E}.Release|x86.Build.0 = Release|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Debug|x64.ActiveCfg = Debug|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Debug|x64.Build.0 = Debug|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Debug|x86.ActiveCfg = Debug|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Debug|x86.Build.0 = Debug|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Release|Any CPU.Build.0 = Release|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Release|x64.ActiveCfg = Release|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Release|x64.Build.0 = Release|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Release|x86.ActiveCfg = Release|Any CPU - {9E574595-A9CD-441A-9328-1D4DD5B531E8}.Release|x86.Build.0 = Release|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Debug|Any CPU.Build.0 = Debug|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Debug|x64.ActiveCfg = Debug|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Debug|x64.Build.0 = Debug|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Debug|x86.ActiveCfg = Debug|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Debug|x86.Build.0 = Debug|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Release|Any CPU.ActiveCfg = Release|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Release|Any CPU.Build.0 = Release|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Release|x64.ActiveCfg = Release|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Release|x64.Build.0 = Release|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Release|x86.ActiveCfg = Release|Any CPU - {644C4B4A-4A32-4307-9F71-C3BF901FFB66}.Release|x86.Build.0 = Release|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Debug|x64.ActiveCfg = Debug|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Debug|x64.Build.0 = Debug|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Debug|x86.ActiveCfg = Debug|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Debug|x86.Build.0 = Debug|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Release|Any CPU.Build.0 = Release|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Release|x64.ActiveCfg = Release|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Release|x64.Build.0 = Release|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Release|x86.ActiveCfg = Release|Any CPU - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF}.Release|x86.Build.0 = Release|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Debug|x64.ActiveCfg = Debug|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Debug|x64.Build.0 = Debug|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Debug|x86.ActiveCfg = Debug|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Debug|x86.Build.0 = Debug|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Release|Any CPU.Build.0 = Release|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Release|x64.ActiveCfg = Release|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Release|x64.Build.0 = Release|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Release|x86.ActiveCfg = Release|Any CPU - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906}.Release|x86.Build.0 = Release|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Debug|Any CPU.Build.0 = Debug|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Debug|x64.ActiveCfg = Debug|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Debug|x64.Build.0 = Debug|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Debug|x86.ActiveCfg = Debug|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Debug|x86.Build.0 = Debug|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Release|Any CPU.ActiveCfg = Release|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Release|Any CPU.Build.0 = Release|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Release|x64.ActiveCfg = Release|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Release|x64.Build.0 = Release|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Release|x86.ActiveCfg = Release|Any CPU - {591EF370-7AD7-4624-8B9D-FD15010CA657}.Release|x86.Build.0 = Release|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Debug|Any CPU.Build.0 = Debug|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Debug|x64.ActiveCfg = Debug|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Debug|x64.Build.0 = Debug|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Debug|x86.ActiveCfg = Debug|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Debug|x86.Build.0 = Debug|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Release|Any CPU.ActiveCfg = Release|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Release|Any CPU.Build.0 = Release|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Release|x64.ActiveCfg = Release|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Release|x64.Build.0 = Release|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Release|x86.ActiveCfg = Release|Any CPU - {10DFE204-B027-49DA-BD77-08ECA18DD357}.Release|x86.Build.0 = Release|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Debug|x64.ActiveCfg = Debug|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Debug|x64.Build.0 = Debug|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Debug|x86.ActiveCfg = Debug|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Debug|x86.Build.0 = Debug|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Release|Any CPU.Build.0 = Release|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Release|x64.ActiveCfg = Release|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Release|x64.Build.0 = Release|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Release|x86.ActiveCfg = Release|Any CPU - {789A05F2-5EF6-4FE8-9609-4706207E047E}.Release|x86.Build.0 = Release|Any CPU {A54EE328-D456-4BAF-A180-84E77E6409AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A54EE328-D456-4BAF-A180-84E77E6409AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {A54EE328-D456-4BAF-A180-84E77E6409AC}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -310,20 +140,8 @@ Global GlobalSection(NestedProjects) = preSolution {0BE8E56E-7580-4526-BE24-D304E1779724} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {F843CCCA-4CC9-422C-A881-3AE6A998B53F} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} - {8A592CB0-5FB9-4E70-A68A-BE5B5BE23C00} = {6749FB3E-39DE-4321-A39E-525278E9408D} - {EEC30462-078F-45EB-AA70-12E3170CD51E} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} - {1F4B2074-F651-4A02-A860-7DDA74B2CC5F} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {E23C760E-B826-4B4F-BE76-916D86BAD2DB} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {E41F0471-B14D-4FA0-9D8B-1E7750695AE9} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} - {8D9A8DCB-DC74-4B3A-B1C6-046C9C4F458E} = {6749FB3E-39DE-4321-A39E-525278E9408D} - {27E3BFFC-4412-4E4C-A656-B9D35B8A0F3E} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} - {9E574595-A9CD-441A-9328-1D4DD5B531E8} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} - {644C4B4A-4A32-4307-9F71-C3BF901FFB66} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} - {39483140-BC26-4CAD-BBAE-3DC76C2F16CF} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} - {0BF6958D-9EE3-4623-B3D6-4DA77EAC1906} = {6749FB3E-39DE-4321-A39E-525278E9408D} - {591EF370-7AD7-4624-8B9D-FD15010CA657} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} - {10DFE204-B027-49DA-BD77-08ECA18DD357} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} - {789A05F2-5EF6-4FE8-9609-4706207E047E} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {A54EE328-D456-4BAF-A180-84E77E6409AC} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {D77D8EE4-7FBA-425C-AEE6-D6908998E228} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {9E93F66A-6099-4675-AF53-FC10DE01925B} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} diff --git a/samples/DragonFruit/DragonFruit.csproj b/samples/DragonFruit/DragonFruit.csproj index 7c72c4da2b..92e9a9cd37 100644 --- a/samples/DragonFruit/DragonFruit.csproj +++ b/samples/DragonFruit/DragonFruit.csproj @@ -6,8 +6,4 @@ true - - - - diff --git a/samples/HostingPlayground/HostingPlayground.csproj b/samples/HostingPlayground/HostingPlayground.csproj index 54bc18bb67..b0060a43ee 100644 --- a/samples/HostingPlayground/HostingPlayground.csproj +++ b/samples/HostingPlayground/HostingPlayground.csproj @@ -8,8 +8,6 @@ - - diff --git a/samples/RenderingPlayground/RenderingPlayground.csproj b/samples/RenderingPlayground/RenderingPlayground.csproj index 109f8622e1..0fdc93b388 100644 --- a/samples/RenderingPlayground/RenderingPlayground.csproj +++ b/samples/RenderingPlayground/RenderingPlayground.csproj @@ -10,9 +10,4 @@ - - - - - diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.cs b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.cs index b413bf2120..0e0a21633c 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.cs +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.cs @@ -1,8 +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.CommandLine.Hosting; -using System.CommandLine.NamingConventionBinder; using ApprovalTests; using ApprovalTests.Reporters; using Xunit; @@ -18,20 +16,4 @@ public void System_CommandLine_api_is_not_changed() var contract = ApiContract.GenerateContractForAssembly(typeof(ParseResult).Assembly); Approvals.Verify(contract); } - - [Fact] - [UseReporter(typeof(DiffReporter))] - public void System_CommandLine_Hosting_api_is_not_changed() - { - var contract = ApiContract.GenerateContractForAssembly(typeof(HostingExtensions).Assembly); - Approvals.Verify(contract); - } - - [Fact] - [UseReporter(typeof(DiffReporter))] - public void System_CommandLine_NamingConventionBinder_api_is_not_changed() - { - var contract = ApiContract.GenerateContractForAssembly(typeof(ModelBindingCommandHandler).Assembly); - Approvals.Verify(contract); - } } \ No newline at end of file diff --git a/src/System.CommandLine.ApiCompatibility.Tests/System.CommandLine.ApiCompatibility.Tests.csproj b/src/System.CommandLine.ApiCompatibility.Tests/System.CommandLine.ApiCompatibility.Tests.csproj index 670b1d0361..b69badcf7c 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/System.CommandLine.ApiCompatibility.Tests.csproj +++ b/src/System.CommandLine.ApiCompatibility.Tests/System.CommandLine.ApiCompatibility.Tests.csproj @@ -10,8 +10,6 @@ - - From 497a1105d9ad38690e724d4b753d5e958bd33729 Mon Sep 17 00:00:00 2001 From: Jean Joeris Date: Sat, 2 Mar 2024 14:00:32 -0500 Subject: [PATCH 022/150] Comment out dotnet-suggest and tests to allow compilation --- .../SuggestionDispatcherTests.cs | 2 ++ .../SuggestionShellScriptHandlerTest.cs | 2 ++ src/System.CommandLine.Suggest/SuggestionDispatcher.cs | 9 ++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/System.CommandLine.Suggest.Tests/SuggestionDispatcherTests.cs b/src/System.CommandLine.Suggest.Tests/SuggestionDispatcherTests.cs index a0a5c2094d..69277aabd3 100644 --- a/src/System.CommandLine.Suggest.Tests/SuggestionDispatcherTests.cs +++ b/src/System.CommandLine.Suggest.Tests/SuggestionDispatcherTests.cs @@ -14,6 +14,7 @@ namespace System.CommandLine.Suggest.Tests { public class SuggestionDispatcherTests { + /* private static readonly string _currentExeName = CliRootCommand.ExecutableName; private static readonly string _dotnetExeFullPath = @@ -237,5 +238,6 @@ public string GetCompletions(string exeFileName, string suggestionTargetArgument return _getSuggestions(exeFileName, suggestionTargetArguments, timeout); } } + */ } } diff --git a/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs b/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs index 151e17eddc..cde8985ceb 100644 --- a/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs +++ b/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs @@ -10,6 +10,7 @@ namespace System.CommandLine.Suggest.Tests { public class SuggestionShellScriptHandlerTest { + /* private readonly CliConfiguration _configuration; public SuggestionShellScriptHandlerTest() @@ -62,5 +63,6 @@ public async Task It_should_print_zsh_shell_script() _configuration.Output.ToString().Should().Contain("_dotnet_zsh_complete()"); _configuration.Output.ToString().Should().NotContain("\r\n"); } + */ } } diff --git a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs index eea48e5a67..34660e1351 100644 --- a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs +++ b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs @@ -6,12 +6,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.CommandLine.Completions; +//using System.CommandLine.Completions; namespace System.CommandLine.Suggest { public class SuggestionDispatcher { + /* private readonly ISuggestionRegistration _suggestionRegistration; private readonly ISuggestionStore _suggestionStore; @@ -289,5 +290,11 @@ public static string FormatSuggestionArguments( return $"{suggestDirective} \"{commandLine.Escape()}\""; } + */ + + // Adding these to allow compilation with code commented out + public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration) { } + + public Task InvokeAsync(string[] args) => throw new NotImplementedException(); } } \ No newline at end of file From ee439787719be32e6663be908adc50858484d881 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 2 Mar 2024 14:13:22 -0500 Subject: [PATCH 023/150] Commented out compiler error --- .../LocalizationTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/System.CommandLine.ApiCompatibility.Tests/LocalizationTests.cs b/src/System.CommandLine.ApiCompatibility.Tests/LocalizationTests.cs index c861914005..0380955eb6 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/LocalizationTests.cs +++ b/src/System.CommandLine.ApiCompatibility.Tests/LocalizationTests.cs @@ -8,6 +8,7 @@ public class LocalizationTests { private const string CommandName = "the-command"; + /* [Theory] [InlineData("es", $"Falta el argumento requerido para el comando: '{CommandName}'.")] [InlineData("en-US", $"Required argument missing for command: '{CommandName}'.")] @@ -33,5 +34,6 @@ public void ErrorMessages_AreLocalized(string cultureName, string expectedMessag CultureInfo.CurrentUICulture = uiCultureBefore; } } + */ } } From f76c352222c0f9c54475d78ec2a90d424487b2f3 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 3 Mar 2024 07:47:41 -0500 Subject: [PATCH 024/150] Skipping api compat test --- .../ApiCompatibilityApprovalTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.cs b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.cs index 0e0a21633c..b6725a8b05 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.cs +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.cs @@ -9,7 +9,7 @@ namespace System.CommandLine.ApiCompatibility.Tests; public class ApiCompatibilityApprovalTests { - [Fact] + [Fact(Skip = "This test to track API changes is turned off as we are aggressively changing the APO")] [UseReporter(typeof(DiffReporter))] public void System_CommandLine_api_is_not_changed() { From 1807616cc5701476d2da7edfa744328f20dd1fbe Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 3 Mar 2024 08:08:31 -0500 Subject: [PATCH 025/150] Created Subsystems/Test projects --- System.CommandLine.sln | 30 +++++++++++++++++++ ...System.CommandLine.Subsystems.Tests.csproj | 9 ++++++ .../System.CommandLine.Subsystems.csproj | 14 +++++++++ 3 files changed, 53 insertions(+) create mode 100644 src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj create mode 100644 src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj diff --git a/System.CommandLine.sln b/System.CommandLine.sln index 23b734a558..ba72db885d 100644 --- a/System.CommandLine.sln +++ b/System.CommandLine.sln @@ -39,6 +39,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Extended EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Extended.Tests", "src\System.CommandLine.Extended.Tests\System.CommandLine.Extended.Tests.csproj", "{9E93F66A-6099-4675-AF53-FC10DE01925B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Subsystems", "src\System.CommandLine.Subsystems\System.CommandLine.Subsystems.csproj", "{D750F504-DEBB-47B1-89AC-BB12B796E7B9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Subsystems.Tests", "src\System.CommandLine.Subsystems.Tests\System.CommandLine.Subsystems.Tests.csproj", "{7D6F74A4-28E4-4B57-8A4B-415A533729A7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -133,6 +137,30 @@ Global {9E93F66A-6099-4675-AF53-FC10DE01925B}.Release|x64.Build.0 = Release|Any CPU {9E93F66A-6099-4675-AF53-FC10DE01925B}.Release|x86.ActiveCfg = Release|Any CPU {9E93F66A-6099-4675-AF53-FC10DE01925B}.Release|x86.Build.0 = Release|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Debug|x64.ActiveCfg = Debug|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Debug|x64.Build.0 = Debug|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Debug|x86.ActiveCfg = Debug|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Debug|x86.Build.0 = Debug|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Release|Any CPU.Build.0 = Release|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Release|x64.ActiveCfg = Release|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Release|x64.Build.0 = Release|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Release|x86.ActiveCfg = Release|Any CPU + {D750F504-DEBB-47B1-89AC-BB12B796E7B9}.Release|x86.Build.0 = Release|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Debug|x64.Build.0 = Debug|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Debug|x86.ActiveCfg = Debug|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Debug|x86.Build.0 = Debug|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Release|Any CPU.Build.0 = Release|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Release|x64.ActiveCfg = Release|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Release|x64.Build.0 = Release|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Release|x86.ActiveCfg = Release|Any CPU + {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -145,6 +173,8 @@ Global {A54EE328-D456-4BAF-A180-84E77E6409AC} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {D77D8EE4-7FBA-425C-AEE6-D6908998E228} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {9E93F66A-6099-4675-AF53-FC10DE01925B} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} + {D750F504-DEBB-47B1-89AC-BB12B796E7B9} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} + {7D6F74A4-28E4-4B57-8A4B-415A533729A7} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5C159F93-800B-49E7-9905-EE09F8B8434A} diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj new file mode 100644 index 0000000000..fa71b7ae6a --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj b/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj new file mode 100644 index 0000000000..b039b50e5d --- /dev/null +++ b/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj @@ -0,0 +1,14 @@ + + + + $(TargetFrameworkForNETSDK) + enable + enable + System.CommandLine + + + + + + + From 6241352f73a63d8cc4a625a5de3333c5840e231a Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sun, 25 Feb 2024 11:52:00 -0500 Subject: [PATCH 026/150] Prototype subsystem annotations model --- .../HelpSubsystem.cs | 23 +++++++++ .../Subsystems/AnnotationId.cs | 9 ++++ .../Annotations/AnnotationAccessor.cs | 19 +++++++ .../Subsystems/Annotations/HelpAnnotations.cs | 13 +++++ .../Subsystems/CliSubsystem.cs | 36 +++++++++++++ .../Subsystems/DefaultAnnotationProvider.cs | 40 +++++++++++++++ .../Subsystems/IAnnotationProvider.cs | 14 ++++++ .../SymbolAnnotationExtensions.cs | 50 +++++++++++++++++++ 8 files changed, 204 insertions(+) create mode 100644 src/System.CommandLine.Subsystems/HelpSubsystem.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/DefaultAnnotationProvider.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs create mode 100644 src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs new file mode 100644 index 0000000000..2ea546ee3f --- /dev/null +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -0,0 +1,23 @@ +// 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.Subsystems.Annotations; +using System.CommandLine.Subsystems; + +namespace System.CommandLine; + +// stub Help subsystem demonstrating annotation model. +// +// usage: +// +// +// var help = new HelpSubsystem(); +// var command = new CliCommand("greet") +// .With(help.Description, "Greet the user"); +// +public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) : CliSubsystem(annotationProvider) +{ + public void SetDescription(CliSymbol symbol, string description) => SetAnnotation(symbol, HelpAnnotations.Description, description); + + public AnnotationAccessor Description => new(this, HelpAnnotations.Description); +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs b/src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs new file mode 100644 index 0000000000..85bf9589b6 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs @@ -0,0 +1,9 @@ +// 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.Subsystems; + +/// +/// Describes the ID and type of an annotation. +/// +public record struct AnnotationId(string Id); diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs new file mode 100644 index 0000000000..622361f754 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs @@ -0,0 +1,19 @@ +// 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.CodeAnalysis; + +namespace System.CommandLine.Subsystems.Annotations; + +/// +/// Allows associating an annotation with a . The annotation will be stored by the accessor's owner . +/// +public struct AnnotationAccessor(CliSubsystem owner, AnnotationId id) +{ + /// + /// The ID of the annotation + /// + public AnnotationId Id { get; } + public readonly void Set(CliSymbol symbol, TValue value) => owner.SetAnnotation(symbol, id, value); + public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, id, out value); +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs new file mode 100644 index 0000000000..34db9160e5 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs @@ -0,0 +1,13 @@ +// 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.Subsystems.Annotations; + +/// +/// IDs for well-known help annotations. +/// +public static class HelpAnnotations +{ + const string Prefix = "Help."; + public static AnnotationId Description { get; } = new(Prefix + nameof(Description)); +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs new file mode 100644 index 0000000000..57755f16fc --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -0,0 +1,36 @@ +// 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.Subsystems.Annotations; +using System.Diagnostics.CodeAnalysis; + +namespace System.CommandLine.Subsystems; + +/// +/// Base class for CLI subsystems. Implements storage of annotations. +/// +/// +public abstract class CliSubsystem(IAnnotationProvider? annotationProvider) +{ + DefaultAnnotationProvider? _defaultProvider; + readonly IAnnotationProvider? _annotationProvider = annotationProvider; + + protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out TValue? value) + { + if (_defaultProvider is not null && _defaultProvider.TryGet(symbol, id, out value)) + { + return true; + } + if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, id, out value)) + { + return true; + } + value = default; + return false; + } + + protected internal void SetAnnotation(CliSymbol symbol, AnnotationId id, T value) + { + (_defaultProvider ??= new DefaultAnnotationProvider ()).Set(symbol, id, value); + } +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/DefaultAnnotationProvider.cs b/src/System.CommandLine.Subsystems/Subsystems/DefaultAnnotationProvider.cs new file mode 100644 index 0000000000..0d8ec37dbc --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/DefaultAnnotationProvider.cs @@ -0,0 +1,40 @@ +// 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.CodeAnalysis; + +namespace System.CommandLine.Subsystems; + +/// +/// Default storage for annotations +/// +class DefaultAnnotationProvider : IAnnotationProvider +{ + record struct AnnotationKey(CliSymbol symbol, string annotationId); + + readonly Dictionary annotations = []; + + public bool TryGet(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out TValue? value) + { + if (annotations.TryGetValue(new AnnotationKey(symbol, id.Id), out var obj)) + { + value = (TValue)obj; + return true; + } + + value = default; + return false; + } + + public void Set(CliSymbol symbol, AnnotationId id, TValue value) + { + if (value is not null) + { + annotations[new AnnotationKey(symbol, id.Id)] = value; + } + else + { + annotations.Remove(new AnnotationKey(symbol, id.Id)); + } + } +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs b/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs new file mode 100644 index 0000000000..4885b1623f --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs @@ -0,0 +1,14 @@ +// 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.CodeAnalysis; + +namespace System.CommandLine.Subsystems; + +/// +/// Alternative storage of annotations, enabling lazy loading and dynamic annotations. +/// +public interface IAnnotationProvider +{ + bool TryGet(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out TValue? value); +} diff --git a/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs new file mode 100644 index 0000000000..dd01d767a3 --- /dev/null +++ b/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs @@ -0,0 +1,50 @@ +// 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.Subsystems.Annotations; + +namespace System.CommandLine; + +/// +/// Extensions for that allow fluent construction of and fluent addition of annotations to objects. +/// +public static class SymbolAnnotationExtensions +{ + public static TSymbol With(this TSymbol symbol, AnnotationAccessor annotation, TValue value) + where TSymbol : CliSymbol + { + annotation.Set(symbol, value); + return symbol; + } + + public static TCommand With(this TCommand command, CliCommand subcommand) + where TCommand : CliCommand + { + command.Add(subcommand); + return command; + } + + public static TCommand With(this TCommand command, CliOption option) + where TCommand : CliCommand + { + command.Add(option); + return command; + } + + public static TCommand With(this TCommand command, CliArgument argument) + where TCommand : CliCommand + { + command.Add(argument); + return command; + } + + public static TCommand With(this TCommand command, params CliSymbol[] symbols) + where TCommand : CliCommand + { + foreach (var symbol in symbols) + { + command.Add(symbol); + } + return command; + } +} From c6e6ea306784999a1442a69f8a8392b7a72a8520 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 3 Mar 2024 08:16:57 -0500 Subject: [PATCH 027/150] Initial Subsystems implementation --- src/System.CommandLine.Subsystems/CliExit.cs | 35 ++++++ .../CompletionSubsystem.cs | 25 +++++ .../ConsoleHack.cs | 34 ++++++ .../ErrorReportingSubsystem.cs | 23 ++++ .../HelpSubsystem.cs | 35 +++++- src/System.CommandLine.Subsystems/Pipeline.cs | 106 ++++++++++++++++++ .../StandardPipeline.cs | 14 +++ .../Subsystems/Annotations/HelpAnnotations.cs | 3 +- .../Annotations/VersionAnnotations.cs | 14 +++ .../Subsystems/CliSubsystem.cs | 95 +++++++++++++++- .../Subsystems/PipelineContext.cs | 16 +++ .../Subsystems/Subsystem.cs | 25 +++++ .../Subsystems/SubsystemKind.cs | 13 +++ .../VersionSubsystem.cs | 64 +++++++++++ .../docs-for-cli-authors.md | 9 ++ .../docs-for-cli-extenders.md | 43 +++++++ src/System.CommandLine/CliExecutable.cs | 6 +- 17 files changed, 549 insertions(+), 11 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/CliExit.cs create mode 100644 src/System.CommandLine.Subsystems/CompletionSubsystem.cs create mode 100644 src/System.CommandLine.Subsystems/ConsoleHack.cs create mode 100644 src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs create mode 100644 src/System.CommandLine.Subsystems/Pipeline.cs create mode 100644 src/System.CommandLine.Subsystems/StandardPipeline.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs create mode 100644 src/System.CommandLine.Subsystems/VersionSubsystem.cs create mode 100644 src/System.CommandLine.Subsystems/docs-for-cli-authors.md create mode 100644 src/System.CommandLine.Subsystems/docs-for-cli-extenders.md diff --git a/src/System.CommandLine.Subsystems/CliExit.cs b/src/System.CommandLine.Subsystems/CliExit.cs new file mode 100644 index 0000000000..7e2731d865 --- /dev/null +++ b/src/System.CommandLine.Subsystems/CliExit.cs @@ -0,0 +1,35 @@ +// 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.Subsystems; + +namespace System.CommandLine; + + // TODO: Consider what info is needed after invocation. If it's the whole pipeline context, consider collapsing this with that class. +public class CliExit +{ + internal CliExit(PipelineContext pipelineContext) + : this(pipelineContext.ParseResult, pipelineContext.AlreadyHandled, pipelineContext.ExitCode) + { } + + private CliExit(ParseResult? parseResult, bool handled, int exitCode) + { + ExitCode = exitCode; + Handled = handled; + ParseResult = parseResult; + } + public ParseResult? ParseResult { get; set; } + + public int ExitCode { get; } + + public static implicit operator int(CliExit cliExit) => cliExit.ExitCode; + + public static implicit operator bool(CliExit cliExit) => !cliExit.Handled; + + + public bool Handled { get; } + + public static CliExit NotRun(ParseResult? parseResult) => new(parseResult, false, 0); + + public static CliExit SuccessfullyHandled(ParseResult? parseResult) => new(parseResult, true, 0); +} diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs new file mode 100644 index 0000000000..65ccdb4e16 --- /dev/null +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -0,0 +1,25 @@ +// 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.Subsystems; + +namespace System.CommandLine; + +public class CompletionSubsystem : CliSubsystem +{ + public CompletionSubsystem(IAnnotationProvider? annotationProvider = null) + : base("Completion", annotationProvider, SubsystemKind.Completion) + { } + + // TODO: Figure out trigger for completions + protected internal override bool GetIsActivated(ParseResult? parseResult) + => parseResult is null + ? false + : false; + + protected internal override CliExit Execute(PipelineContext pipelineContext) + { + pipelineContext.ConsoleHack.WriteLine("Not yet implemented"); + return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + } +} diff --git a/src/System.CommandLine.Subsystems/ConsoleHack.cs b/src/System.CommandLine.Subsystems/ConsoleHack.cs new file mode 100644 index 0000000000..78548b6b4f --- /dev/null +++ b/src/System.CommandLine.Subsystems/ConsoleHack.cs @@ -0,0 +1,34 @@ +// 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.Text; + +namespace System.CommandLine; + +public class ConsoleHack +{ + private readonly StringBuilder buffer = new(); + private bool redirecting = false; + + public void WriteLine(string text) + { + if (redirecting) + { + buffer.AppendLine(text); + } + else + { + Console.WriteLine(text); + } + } + + public string GetBuffer() => buffer.ToString(); + + public void ClearBuffer() => buffer.Clear(); + + public ConsoleHack RedirectToBuffer(bool shouldRedirect) + { + redirecting = shouldRedirect; + return this; + } +} diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs new file mode 100644 index 0000000000..1a79e6eba8 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -0,0 +1,23 @@ +// 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.Subsystems; + +namespace System.CommandLine; + +public class ErrorReportingSubsystem : CliSubsystem +{ + public ErrorReportingSubsystem(IAnnotationProvider? annotationProvider = null) + : base("ErrorReporting", annotationProvider, SubsystemKind.ErrorReporting) + { } + + // TODO: Stash option rather than using string + protected internal override bool GetIsActivated(ParseResult? parseResult) + => parseResult is not null && parseResult.Errors.Any(); + + protected internal override CliExit Execute(PipelineContext pipelineContext) + { + pipelineContext.ConsoleHack.WriteLine("You have errors!"); + return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + } +} diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index 2ea546ee3f..9fc8c15997 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -15,9 +15,38 @@ namespace System.CommandLine; // var command = new CliCommand("greet") // .With(help.Description, "Greet the user"); // -public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) : CliSubsystem(annotationProvider) +public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) + : CliSubsystem(HelpAnnotations.Prefix, annotationProvider: annotationProvider, SubsystemKind.Help) { - public void SetDescription(CliSymbol symbol, string description) => SetAnnotation(symbol, HelpAnnotations.Description, description); + public void SetDescription(CliSymbol symbol, string description) + => SetAnnotation(symbol, HelpAnnotations.Description, description); - public AnnotationAccessor Description => new(this, HelpAnnotations.Description); + public string GetDescription(CliSymbol symbol) + => TryGetAnnotation(symbol, HelpAnnotations.Description, out var value) + ? value + : ""; + + public AnnotationAccessor Description + => new(this, HelpAnnotations.Description); + + protected internal override CliConfiguration Initialize(CliConfiguration configuration) + { + var option = new CliOption("--help", ["-h"]) + { + Arity = ArgumentArity.Zero + }; + configuration.RootCommand.Add(option); + + return configuration; + } + + protected internal override bool GetIsActivated(ParseResult? parseResult) + => parseResult is not null && parseResult.GetValue("--help"); + + protected internal override CliExit Execute(PipelineContext pipelineContext) + { + // TODO: Match testable output pattern + pipelineContext.ConsoleHack.WriteLine("Help me!"); + return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + } } diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs new file mode 100644 index 0000000000..00cc1c1dd4 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -0,0 +1,106 @@ +// 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.Parsing; +using System.CommandLine.Subsystems; + +namespace System.CommandLine; + +public class Pipeline +{ + public HelpSubsystem? Help { get; set; } + public VersionSubsystem? Version { get; set; } + public ErrorReportingSubsystem? ErrorReporting { get; set; } + public CompletionSubsystem? Completion { get; set; } + + protected virtual void InitializeHelp(CliConfiguration configuration) + => Help?.Initialize(configuration); + + protected virtual void InitializeVersion(CliConfiguration configuration) + => Version?.Initialize(configuration); + + protected virtual void InitializeErrorReporting(CliConfiguration configuration) + => ErrorReporting?.Initialize(configuration); + + protected virtual void InitializeCompletions(CliConfiguration configuration) + => Completion?.Initialize(configuration); + + protected virtual void TearDownHelp(PipelineContext context) + => Help?.TearDown(context); + + protected virtual void TearDownVersion(PipelineContext context) + => Version?.TearDown(context); + + protected virtual void TearDownErrorReporting(PipelineContext context) + => ErrorReporting?.TearDown(context); + + protected virtual void TearDownCompletions(PipelineContext context) + => Completion?.TearDown(context); + + protected virtual void ExecuteHelp(PipelineContext context) + => ExecuteIfNeeded(Help, context); + + protected virtual void ExecuteVersion(PipelineContext context) + => ExecuteIfNeeded(Version, context); + + protected virtual void ExecuteErrorReporting(PipelineContext context) + => ExecuteIfNeeded(ErrorReporting, context); + + protected virtual void ExecuteCompletions(PipelineContext context) + => ExecuteIfNeeded(Completion, context); + + protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineContext pipelineContext) + { + if (subsystem is not null && (!pipelineContext.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) + { + subsystem.ExecuteIfNeeded(pipelineContext); + } + } + + public virtual void InitializeExtensions(CliConfiguration configuration) + { + InitializeHelp(configuration); + InitializeVersion(configuration); + InitializeErrorReporting(configuration); + InitializeCompletions(configuration); + } + + public virtual void TearDownExtensions(PipelineContext pipelineContext) + { + TearDownHelp(pipelineContext); + TearDownVersion(pipelineContext); + TearDownErrorReporting(pipelineContext); + TearDownCompletions(pipelineContext); + } + + protected virtual void ExecuteRequestedExtensions(PipelineContext pipelineContext) + { + ExecuteHelp(pipelineContext); + ExecuteVersion(pipelineContext); + ExecuteErrorReporting(pipelineContext); + ExecuteCompletions(pipelineContext); + } + + public ParseResult Parse(CliConfiguration configuration, string rawInput) + => Parse(configuration, CliParser.SplitCommandLine(rawInput).ToArray()); + + public ParseResult Parse(CliConfiguration configuration, string[] args) + { + InitializeExtensions(configuration); + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); + return parseResult; + } + + public CliExit Execute(CliConfiguration configuration, string rawInput, ConsoleHack? consoleHack = null) + => Execute(configuration, CliParser.SplitCommandLine(rawInput).ToArray(), rawInput, consoleHack); + + public CliExit Execute(CliConfiguration configuration, string[] args, string rawInput, ConsoleHack? consoleHack = null) + => Execute(Parse(configuration, args), rawInput, consoleHack); + + public CliExit Execute(ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) + { + var pipelineContext = new PipelineContext(parseResult, rawInput, this, consoleHack ?? new ConsoleHack()); + ExecuteRequestedExtensions(pipelineContext); + return new CliExit(pipelineContext); + } +} diff --git a/src/System.CommandLine.Subsystems/StandardPipeline.cs b/src/System.CommandLine.Subsystems/StandardPipeline.cs new file mode 100644 index 0000000000..d1ab7a65cd --- /dev/null +++ b/src/System.CommandLine.Subsystems/StandardPipeline.cs @@ -0,0 +1,14 @@ +// 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; + +public class StandardPipeline : Pipeline +{ + public StandardPipeline() { + Help = new HelpSubsystem(); + Version = new VersionSubsystem(); + ErrorReporting = new ErrorReportingSubsystem(); + Completion = new CompletionSubsystem(); + } +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs index 34db9160e5..3d7eee1744 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs @@ -8,6 +8,7 @@ namespace System.CommandLine.Subsystems.Annotations; /// public static class HelpAnnotations { - const string Prefix = "Help."; + // I made this public because we need an identifier for subsystem Kind, and I think this makes a very good one. + public static string Prefix { get; } = "Help."; public static AnnotationId Description { get; } = new(Prefix + nameof(Description)); } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs new file mode 100644 index 0000000000..e8b411821a --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs @@ -0,0 +1,14 @@ +// 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.Subsystems.Annotations; + +/// +/// IDs for well-known Version annotations. +/// +public static class VersionAnnotations +{ + // I made this public because we need an identifier for subsystem Kind, and I think this makes a very good one. + public static string Prefix { get; } = "Version."; // TODO: Decide between const and property. Isn't a const baked in at the callsite? Would that be OK when this is public? + public static AnnotationId Version { get; } = new(Prefix + nameof(Version)); +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 57755f16fc..6bd755f69e 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -10,11 +10,36 @@ namespace System.CommandLine.Subsystems; /// Base class for CLI subsystems. Implements storage of annotations. /// /// -public abstract class CliSubsystem(IAnnotationProvider? annotationProvider) +public abstract class CliSubsystem { + protected CliSubsystem(string name, IAnnotationProvider? annotationProvider, SubsystemKind subsystemKind) + { + Name = name; + _annotationProvider = annotationProvider; + SubsystemKind = subsystemKind; + } + + /// + /// The name of the subsystem. + /// + public string Name { get; } + public SubsystemKind SubsystemKind { get; } + DefaultAnnotationProvider? _defaultProvider; - readonly IAnnotationProvider? _annotationProvider = annotationProvider; + readonly IAnnotationProvider? _annotationProvider; + /// + /// Attempt to retrieve the value for the symbol and annotation ID from the annotation provider. + /// + /// The value of the type to retrieve + /// The symbol the value is attached to + /// The id for the value to be retrieved. For example, an annotation ID for help is description + /// An out parameter to contain the result + /// True if successful + /// + /// This value is protected because these values are always retrieved from derived classes that offer + /// strongly typed explicit methods, such as help having `GetDescription(Symbol symbol)` method. + /// protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out TValue? value) { if (_defaultProvider is not null && _defaultProvider.TryGet(symbol, id, out value)) @@ -29,8 +54,70 @@ protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId< return false; } - protected internal void SetAnnotation(CliSymbol symbol, AnnotationId id, T value) + /// + /// Set the value for the symbol and annotation ID in the annotation provider. + /// + /// The value of the type to set + /// The symbol the value is attached to + /// The id for the value to be set. For example, an annotation ID for help is description + /// An out parameter to contain the result + /// + /// This value is protected because these values are always retrieved from derived classes that offer + /// strongly typed explicit methods, such as help having `GAetDescription(Symbol symbol, "My help descrption")` method. + /// + protected internal void SetAnnotation(CliSymbol symbol, AnnotationId id, TValue value) { - (_defaultProvider ??= new DefaultAnnotationProvider ()).Set(symbol, id, value); + (_defaultProvider ??= new DefaultAnnotationProvider()).Set(symbol, id, value); } + + protected internal virtual bool RunsEvenIfAlreadyHandled { get; protected set; } + + /// + /// Executes the behavior of the subsystem. For example, help would write information to the console. + /// + /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate + /// A CliExit object with information such as whether the CLI should terminate + protected internal virtual CliExit Execute(PipelineContext pipelineContext) => CliExit.NotRun(pipelineContext.ParseResult); + + internal PipelineContext ExecuteIfNeeded(PipelineContext pipelineContext) + => ExecuteIfNeeded(pipelineContext.ParseResult, pipelineContext); + + internal PipelineContext ExecuteIfNeeded(ParseResult? parseResult, PipelineContext pipelineContext) + { + if( GetIsActivated(parseResult)) + { + Execute(pipelineContext ); + } + return pipelineContext; + } + + + /// + /// Indicates to invocation patterns that the extension should be run. + /// + /// + /// This may be explicitly set, such as a directive like Diagram, or it may explore the result + /// + /// The parse result. + /// + protected internal virtual bool GetIsActivated(ParseResult? parseResult) => false; + + /// + /// Runs before parsing to prepare the parser. Since it always runs, slow code that is only needed when the extension + /// runs as part of invocation should be delayed to BeforeRun(). Default behavior is to do nothing. + /// + /// + /// Use cases: + /// * Add to the CLI, such as adding version option + /// * Early setup of extension internal data, such as reading a file that contains defaults + /// * Licensing if early exit is needed + /// + /// The CLI configuration, which contains the RootCommand for customization + /// True if parsing should continue // there might be a better design that supports a message + // TODO: Because of this and similar usage, consider combining CLI declaration and config. ArgParse calls this the parser, which I like + protected internal virtual CliConfiguration Initialize(CliConfiguration configuration) => configuration; + + // TODO: Determine if this is needed. + protected internal virtual PipelineContext TearDown(PipelineContext pipelineContext) => pipelineContext; + } diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs b/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs new file mode 100644 index 0000000000..847c328678 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs @@ -0,0 +1,16 @@ +// 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.Subsystems; + +public class PipelineContext(ParseResult? parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) +{ + public ParseResult? ParseResult { get; } = parseResult; + public string RawInput { get; } = rawInput; + public Pipeline Pipeline { get; } = pipeline ?? new Pipeline(); + public ConsoleHack ConsoleHack { get; } = consoleHack ?? new ConsoleHack(); + + public bool AlreadyHandled { get; set; } + public int ExitCode { get; set; } + +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs new file mode 100644 index 0000000000..89295c0c7d --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs @@ -0,0 +1,25 @@ +// 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.Subsystems; + +public class Subsystem +{ + public static void Initialize(CliSubsystem subsystem, CliConfiguration configuration) + => subsystem.Initialize(configuration); + + public static CliExit Execute(CliSubsystem subsystem, PipelineContext pipelineContext) + => subsystem.Execute(pipelineContext); + + public static bool GetIsActivated(CliSubsystem subsystem, ParseResult parseResult) + => subsystem.GetIsActivated(parseResult); + + public static CliExit ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) + => new(subsystem.ExecuteIfNeeded(new PipelineContext(parseResult, rawInput, null,consoleHack))); + + internal static PipelineContext ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack, PipelineContext? pipelineContext = null) + => subsystem.ExecuteIfNeeded(pipelineContext ?? new PipelineContext(parseResult, rawInput, null,consoleHack)); + + internal static PipelineContext ExecuteIfNeeded(CliSubsystem subsystem, PipelineContext pipelineContext) + => subsystem.ExecuteIfNeeded(pipelineContext); +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs new file mode 100644 index 0000000000..8e6b80f3b2 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs @@ -0,0 +1,13 @@ +// 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.Subsystems; + +public enum SubsystemKind +{ + Other = 0, + Help, + Version, + ErrorReporting, + Completion, +} diff --git a/src/System.CommandLine.Subsystems/VersionSubsystem.cs b/src/System.CommandLine.Subsystems/VersionSubsystem.cs new file mode 100644 index 0000000000..edec5c2eb8 --- /dev/null +++ b/src/System.CommandLine.Subsystems/VersionSubsystem.cs @@ -0,0 +1,64 @@ +// 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.Subsystems; +using System.Reflection; + +namespace System.CommandLine; + +public class VersionSubsystem : CliSubsystem +{ + private string? specificVersion = null; + + public VersionSubsystem(IAnnotationProvider? annotationProvider = null) + : base("Version", annotationProvider, SubsystemKind.Version) + { + } + + + // TODO: Should we block adding version anywhere but root? + public string? SpecificVersion + { + get + { + var version = specificVersion is null + ? AssemblyVersion(Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) + : specificVersion; + return version ?? ""; + } + set => specificVersion = value; + } + + public static string? AssemblyVersion(Assembly assembly) + => assembly + ?.GetCustomAttribute() + ?.InformationalVersion; + + + protected internal override CliConfiguration Initialize(CliConfiguration configuration) + { + var option = new CliOption("--version", ["-v"]) + { + Arity = ArgumentArity.Zero + }; + configuration.RootCommand.Add(option); + + return configuration; + } + + // TODO: Stash option rather than using string + protected internal override bool GetIsActivated(ParseResult? parseResult) + => parseResult is not null && parseResult.GetValue("--version"); + + protected internal override CliExit Execute(PipelineContext pipelineContext) + { + var subsystemVersion = SpecificVersion; + var version = subsystemVersion is null + ? CliExecutable.ExecutableVersion + : subsystemVersion; + pipelineContext.ConsoleHack.WriteLine(version); + pipelineContext.AlreadyHandled = true; + return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + } +} + diff --git a/src/System.CommandLine.Subsystems/docs-for-cli-authors.md b/src/System.CommandLine.Subsystems/docs-for-cli-authors.md new file mode 100644 index 0000000000..c6eca3b19b --- /dev/null +++ b/src/System.CommandLine.Subsystems/docs-for-cli-authors.md @@ -0,0 +1,9 @@ +# Docs for CLI authors + +This is a space for tentative aspirational documentation + +## Basic usage for full featured parser + +* Create a custom parser by adding options, arguments and commands + * The process of adding options, arguments and commands is the same for the parser and subcommands +* Run StandardPipeline.Execute(args) \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems/docs-for-cli-extenders.md b/src/System.CommandLine.Subsystems/docs-for-cli-extenders.md new file mode 100644 index 0000000000..04c52ee956 --- /dev/null +++ b/src/System.CommandLine.Subsystems/docs-for-cli-extenders.md @@ -0,0 +1,43 @@ +# Docs for folks extending Systemm.CommandLine subsystem + +This is a space for tentative aspirational documentation + +There are a few ways to extend System.CommandLine subsystems + +* Replace an existing subsystem, such as replacing Help. +* Add a new subsystem we did not implement. +* Supply multiple subsystems for an existing category, such as running multiple Help subsystems. + +This design is based on the following assumptions: + +* There will be between 10 and 10,000 CLI authors for every extender. +* There will be more replacement of existing subsystems than creation of new ones. +* CLI authors will often want to replace subsystems, especially help. +* Some folks will want extreme extensibility. +* Data needs to be exchanged between subsystems (this is the area of most significant change from prior versions). + +## Replacing an existing subsystem or adding a new one + +* Inherit from the existing subsystem or CliSubsystem +* Override `GetIsActivated`, unless your subsystem should never run (such as you have initialization only behavior): + * You will generally not need to do this except for new subsystems that need to add triggers. + * If your subsystem should run even if another subsystem has handled execution (extremely rare), set `ExecuteEvenIfAlreadyHandled` +* Override `Initialize` if needed: + * You will generally not need to do this except for new subsystems that need to respond to their triggers. + * Delay as much work as possible until it is actually needed. +* Override `Execute`: + * Ensure that output is sent to `Console` on the pipeline, not directly to `StdOut`, `StdErr` or `System.Console` +* To manage data: + * For every piece data value, create a public `Get...` and `Set...` method using the accessor pattern that allows CLI authors to use the `With` extension method and implicitly converts to string (replace "Description" with the name of your data value in 6 places and possibly change the type in 2 places): + +```csharp + public void SetDescription(CliSymbol symbol, string description) + => SetAnnotation(symbol, HelpAnnotations.Description, description); + + public AnnotationAccessor Description + => new(this, HelpAnnotations.Description); +``` + +* Let folks know to add your subsystem, or provide an alternative to StandardPipeline. + + diff --git a/src/System.CommandLine/CliExecutable.cs b/src/System.CommandLine/CliExecutable.cs index 96b9908626..52816e9d93 100644 --- a/src/System.CommandLine/CliExecutable.cs +++ b/src/System.CommandLine/CliExecutable.cs @@ -6,11 +6,11 @@ namespace System.CommandLine; -//TODO: cull unused member, consider making public again +//TODO: cull unused member, consider making public again. KAD: Made public because used by version. If not needed in core, move to S.CL.Subsystems /// /// Static helpers for determining information about the CLI executable. /// -internal static class CliExecutable +public static class CliExecutable { private static Assembly? _assembly; private static string? _executablePath; @@ -31,7 +31,7 @@ public static string ExecutableName /// public static string ExecutablePath => _executablePath ??= Environment.GetCommandLineArgs()[0]; - internal static string ExecutableVersion => _executableVersion ??= GetExecutableVersion(); + public static string ExecutableVersion => _executableVersion ??= GetExecutableVersion(); private static string GetExecutableVersion() { From a094511c5c1146c0bae93c75739a57f258d6d249 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 3 Mar 2024 13:53:35 -0500 Subject: [PATCH 028/150] Moved subsystem test files out of Extended.Tests --- .../System.CommandLine.Extended.Tests.csproj | 1 - .../CompletionTests.cs | 0 .../Help/ApprovalTests.Config.cs | 0 .../HelpBuilderTests.Help_layout_has_not_changed.approved.txt | 0 .../Help/HelpBuilderExtensions.cs | 0 .../Help/HelpBuilderTests.Approval.cs | 0 .../Help/HelpBuilderTests.Customization.cs | 0 .../Help/HelpBuilderTests.cs | 0 .../HelpOptionTests.cs | 0 .../VersionOptionTests.cs | 0 10 files changed, 1 deletion(-) rename src/{System.CommandLine.Extended.Tests => System.CommandLine.Subsystems.Tests}/CompletionTests.cs (100%) rename src/{System.CommandLine.Extended.Tests => System.CommandLine.Subsystems.Tests}/Help/ApprovalTests.Config.cs (100%) rename src/{System.CommandLine.Extended.Tests => System.CommandLine.Subsystems.Tests}/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt (100%) rename src/{System.CommandLine.Extended.Tests => System.CommandLine.Subsystems.Tests}/Help/HelpBuilderExtensions.cs (100%) rename src/{System.CommandLine.Extended.Tests => System.CommandLine.Subsystems.Tests}/Help/HelpBuilderTests.Approval.cs (100%) rename src/{System.CommandLine.Extended.Tests => System.CommandLine.Subsystems.Tests}/Help/HelpBuilderTests.Customization.cs (100%) rename src/{System.CommandLine.Extended.Tests => System.CommandLine.Subsystems.Tests}/Help/HelpBuilderTests.cs (100%) rename src/{System.CommandLine.Extended.Tests => System.CommandLine.Subsystems.Tests}/HelpOptionTests.cs (100%) rename src/{System.CommandLine.Extended.Tests => System.CommandLine.Subsystems.Tests}/VersionOptionTests.cs (100%) diff --git a/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj b/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj index 5506d5b74a..f194d83ce7 100644 --- a/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj +++ b/src/System.CommandLine.Extended.Tests/System.CommandLine.Extended.Tests.csproj @@ -22,7 +22,6 @@ - diff --git a/src/System.CommandLine.Extended.Tests/CompletionTests.cs b/src/System.CommandLine.Subsystems.Tests/CompletionTests.cs similarity index 100% rename from src/System.CommandLine.Extended.Tests/CompletionTests.cs rename to src/System.CommandLine.Subsystems.Tests/CompletionTests.cs diff --git a/src/System.CommandLine.Extended.Tests/Help/ApprovalTests.Config.cs b/src/System.CommandLine.Subsystems.Tests/Help/ApprovalTests.Config.cs similarity index 100% rename from src/System.CommandLine.Extended.Tests/Help/ApprovalTests.Config.cs rename to src/System.CommandLine.Subsystems.Tests/Help/ApprovalTests.Config.cs diff --git a/src/System.CommandLine.Extended.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt b/src/System.CommandLine.Subsystems.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt similarity index 100% rename from src/System.CommandLine.Extended.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt rename to src/System.CommandLine.Subsystems.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt diff --git a/src/System.CommandLine.Extended.Tests/Help/HelpBuilderExtensions.cs b/src/System.CommandLine.Subsystems.Tests/Help/HelpBuilderExtensions.cs similarity index 100% rename from src/System.CommandLine.Extended.Tests/Help/HelpBuilderExtensions.cs rename to src/System.CommandLine.Subsystems.Tests/Help/HelpBuilderExtensions.cs diff --git a/src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.Approval.cs b/src/System.CommandLine.Subsystems.Tests/Help/HelpBuilderTests.Approval.cs similarity index 100% rename from src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.Approval.cs rename to src/System.CommandLine.Subsystems.Tests/Help/HelpBuilderTests.Approval.cs diff --git a/src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.Customization.cs b/src/System.CommandLine.Subsystems.Tests/Help/HelpBuilderTests.Customization.cs similarity index 100% rename from src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.Customization.cs rename to src/System.CommandLine.Subsystems.Tests/Help/HelpBuilderTests.Customization.cs diff --git a/src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.cs b/src/System.CommandLine.Subsystems.Tests/Help/HelpBuilderTests.cs similarity index 100% rename from src/System.CommandLine.Extended.Tests/Help/HelpBuilderTests.cs rename to src/System.CommandLine.Subsystems.Tests/Help/HelpBuilderTests.cs diff --git a/src/System.CommandLine.Extended.Tests/HelpOptionTests.cs b/src/System.CommandLine.Subsystems.Tests/HelpOptionTests.cs similarity index 100% rename from src/System.CommandLine.Extended.Tests/HelpOptionTests.cs rename to src/System.CommandLine.Subsystems.Tests/HelpOptionTests.cs diff --git a/src/System.CommandLine.Extended.Tests/VersionOptionTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionOptionTests.cs similarity index 100% rename from src/System.CommandLine.Extended.Tests/VersionOptionTests.cs rename to src/System.CommandLine.Subsystems.Tests/VersionOptionTests.cs From 25a798357beb7aface52436eab6a30ea5410cf5e Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 3 Mar 2024 15:41:23 -0500 Subject: [PATCH 029/150] Updated Subsystems.Test project --- ...System.CommandLine.Subsystems.Tests.csproj | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index fa71b7ae6a..9d2460d1cb 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -1,9 +1,49 @@  - - net8.0 - enable - enable + + $(TargetFrameworkForNETSDK);net462 + false + $(DefaultExcludesInProjectFolder);TestApps\** + Library + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 99b88b242f76f1b8e5ff5ea15966334be5ebe2c2 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 3 Mar 2024 16:01:16 -0500 Subject: [PATCH 030/150] Add VersionSubsystem tests Removed netstandard and added file to get compilation Renamed old tests to functional tests to distinguish from new unit tests Comments and TODOs for commented out tests --- .../AlternateSubsystems.cs | 43 ++++++ ...System.CommandLine.Subsystems.Tests.csproj | 14 +- ...tionTests.cs => VersionFunctionalTests.cs} | 62 ++++++--- .../VersionSubsystemTests.cs | 131 ++++++++++++++++++ 4 files changed, 222 insertions(+), 28 deletions(-) create mode 100644 src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs rename src/System.CommandLine.Subsystems.Tests/{VersionOptionTests.cs => VersionFunctionalTests.cs} (70%) create mode 100644 src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs new file mode 100644 index 0000000000..759e442954 --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -0,0 +1,43 @@ +// 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.Subsystems; + +namespace System.CommandLine.Subsystems.Tests +{ + internal class AlternateSubsystems + { + internal class Version : VersionSubsystem + { + protected override CliExit Execute(PipelineContext pipelineContext) + { + pipelineContext.ConsoleHack.WriteLine($"***{CliExecutable.ExecutableVersion}***"); + pipelineContext.AlreadyHandled = true; + return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + } + } + + internal class VersionThatUsesHelpData : VersionSubsystem + { + // for testing, this class accepts a symbol and accesses its description + + public VersionThatUsesHelpData(CliSymbol symbol) + { + Symbol = symbol; + } + + private CliSymbol Symbol { get; } + + protected override CliExit Execute(PipelineContext pipelineContext) + { + var help = pipelineContext.Pipeline.Help ?? throw new InvalidOperationException("Help cannot be null for this subsystem to work"); + var data = help.GetDescription(Symbol); + + pipelineContext.ConsoleHack.WriteLine(data); + pipelineContext.AlreadyHandled = true; + return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + } + } + + } +} diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index 9d2460d1cb..70f5d1432d 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -1,10 +1,12 @@  - $(TargetFrameworkForNETSDK);net462 + $(TargetFrameworkForNETSDK) false $(DefaultExcludesInProjectFolder);TestApps\** Library + enable + enable False @@ -22,12 +24,15 @@ - + + + + - + @@ -42,8 +47,7 @@ - + diff --git a/src/System.CommandLine.Subsystems.Tests/VersionOptionTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs similarity index 70% rename from src/System.CommandLine.Subsystems.Tests/VersionOptionTests.cs rename to src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs index d0d1b28499..48784745ab 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionOptionTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs @@ -1,37 +1,37 @@ // 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.IO; using System.Reflection; -using System.Threading.Tasks; -using System.Linq; using FluentAssertions; using Xunit; using System.CommandLine.Parsing; -namespace System.CommandLine.Extended.Tests +namespace System.CommandLine.Subsystems.Tests { - public class VersionOptionTests + public class VersionFunctionalTests { -// TODO: invocation/output -/* - private static readonly string version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) - .GetCustomAttribute() - .InformationalVersion; + private static readonly string? version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) + ?.GetCustomAttribute() + ?.InformationalVersion; + private readonly string newLine = Environment.NewLine; [Fact] - public async Task When_the_version_option_is_specified_then_the_version_is_written_to_standard_out() + public void When_the_version_option_is_specified_then_the_version_is_written_to_standard_out() { - CliConfiguration configuration = new(new CliRootCommand()) - { - Output = new StringWriter() - }; + var configuration = new CliConfiguration(new CliRootCommand()); + var pipeline = new Pipeline(); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + pipeline.Version = new VersionSubsystem(); - await configuration.InvokeAsync("--version"); + var exit = pipeline.Execute(configuration, "-v", consoleHack); - configuration.Output.ToString().Should().Be($"{version}{NewLine}"); + exit.ExitCode.Should().Be(0); + exit.Handled.Should().BeTrue(); + consoleHack.GetBuffer().Should().Be($"{version}{newLine}"); } + // TODO: invocation/output + /* [Fact] public async Task When_the_version_option_is_specified_then_invocation_is_short_circuited() { @@ -48,7 +48,9 @@ public async Task When_the_version_option_is_specified_then_invocation_is_short_ wasCalled.Should().BeFalse(); } + */ + /* Consider removing this test as it appears to test that the version option is added by default [Fact] public void When_the_version_option_is_specified_then_the_version_is_parsed() { @@ -59,7 +61,10 @@ public void When_the_version_option_is_specified_then_the_version_is_parsed() parseResult.Errors.Should().BeEmpty(); parseResult.GetValue(configuration.RootCommand.Options.OfType().Single()).Should().BeTrue(); } + */ + // TODO: Help + /* [Fact] public async Task Version_option_appears_in_help() { @@ -71,13 +76,15 @@ public async Task Version_option_appears_in_help() await configuration.InvokeAsync("--help"); configuration.Output - .ToString() - .Should() - .Match("*Options:*--version*Show version information*"); + .ToString() + .Should() + .Match("*Options:*--version*Show version information*"); } + // TODO: Defaults. These two tests appear to test whether the presence of a default factory on a different option breaks version + /* [Fact] - public async Task When_the_version_option_is_specified_and_there_are_default_options_then_the_version_is_written_to_standard_out() + public void When_the_version_option_is_specified_and_there_are_default_options_then_the_version_is_written_to_standard_out() { var rootCommand = new CliRootCommand { @@ -116,13 +123,14 @@ public async Task When_the_version_option_is_specified_and_there_are_default_arg configuration.Output.ToString().Should().Be($"{version}{NewLine}"); } -*/ + */ const string SkipValidationTests = "VersionOption does not yet do validation"; [Theory] [InlineData("--version", "-x", Skip = SkipValidationTests)] [InlineData("--version", "subcommand", Skip = SkipValidationTests)] + // TODO: This test will fail because it expects version to always be added public void Version_is_not_valid_with_other_tokens(params string[] commandLine) { var subcommand = new CliCommand("subcommand"); @@ -136,8 +144,9 @@ public void Version_is_not_valid_with_other_tokens(params string[] commandLine) result.Errors.Should().Contain(e => e.Message == "--version option cannot be combined with other arguments."); } - + [Fact] + // TODO: This test will fail because it expects version to always be added public void Version_option_is_not_added_to_subcommands() { var childCommand = new CliCommand("subcommand"); @@ -155,6 +164,8 @@ public void Version_option_is_not_added_to_subcommands() .BeEmpty(); } + // TODO: Determine Ux for adding more aliases. There is no easy access point for the user to access the option, and not much reason to. Consider requiring override or possibly extra property. + /* [Fact] public void Version_can_specify_additional_alias() { @@ -169,8 +180,12 @@ public void Version_can_specify_additional_alias() versionSpecified = parseResult.GetValue(versionOption); versionSpecified.Should().BeTrue(); } + */ + // TODO: Determine if the limitation to root is desirable + /* [Fact(Skip = SkipValidationTests)] + // TODO: This test will fail because it expects version to always be added public void Version_is_not_valid_with_other_tokens_uses_custom_alias() { var childCommand = new CliCommand("subcommand"); @@ -185,5 +200,6 @@ public void Version_is_not_valid_with_other_tokens_uses_custom_alias() result.Errors.Should().ContainSingle(e => e.Message == "-v option cannot be combined with other arguments."); } + */ } } \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs new file mode 100644 index 0000000000..7af1769880 --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs @@ -0,0 +1,131 @@ +// 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.Reflection; +using FluentAssertions; +using Xunit; +using System.CommandLine.Parsing; + +namespace System.CommandLine.Subsystems.Tests +{ + public class VersionSubsystemTests + { + private static readonly string? version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) + ?.GetCustomAttribute() + ?.InformationalVersion; + + private readonly string newLine = Environment.NewLine; + + + [Fact] + public void When_version_subsystem_is_used_the_version_option_is_added_to_the_root() + { + var rootCommand = new CliRootCommand + { + new CliOption("-x") + }; + var configuration = new CliConfiguration(rootCommand); + var pipeline = new Pipeline + { + Version = new VersionSubsystem() + }; + + // Parse is used because directly calling Initialize would be unusual + var result = pipeline.Parse(configuration, ""); + + rootCommand.Options.Should().NotBeNull(); + rootCommand.Options + .Count(x => x.Name == "--version") + .Should() + .Be(1); + + } + + [Theory] + [InlineData("--version", true)] + [InlineData("-v", true)] + [InlineData("-x", false)] + [InlineData("", false)] + public void Version_is_activated_only_when_requested(string input, bool result) + { + CliRootCommand rootCommand = new(); + var configuration = new CliConfiguration(rootCommand); + var versionSubsystem = new VersionSubsystem(); + Subsystem.Initialize(versionSubsystem, configuration); + + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var isActive = Subsystem.GetIsActivated(versionSubsystem, parseResult); + + isActive.Should().Be(result); + } + + [Fact] + public void Outputs_assembly_version() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem(); + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be(version); + } + + [Fact] + public void Outputs_specified_version() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem + { + SpecificVersion = "42" + }; + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be("42"); + } + + [Fact] + public void Outputs_assembly_version_when_specified_version_set_to_null() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem + { + SpecificVersion = null + }; + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be(version); + } + + [Fact] + public void Console_output_can_be_tested() + { + CliConfiguration configuration = new(new CliRootCommand()) + { }; + + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem(); + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be(version); + } + + [Fact] + public void Custom_version_subsystem_can_be_used() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var pipeline = new Pipeline + { + Version = new AlternateSubsystems.Version() + }; + pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); + consoleHack.GetBuffer().Trim().Should().Be($"***{version}***"); + } + + [Fact] + public void Custom_version_subsystem_can_replace_standard() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var pipeline = new StandardPipeline + { + Version = new AlternateSubsystems.Version() + }; + pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); + consoleHack.GetBuffer().Trim().Should().Be($"***{version}***"); + } + } +} From d9ed7392217d54627ed103147c6aaa2680e048f2 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 3 Mar 2024 16:02:10 -0500 Subject: [PATCH 031/150] Assigned RootCommand until we straighten out top level --- src/System.CommandLine/CliConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/System.CommandLine/CliConfiguration.cs b/src/System.CommandLine/CliConfiguration.cs index cd63a01c20..b5a4e09e26 100644 --- a/src/System.CommandLine/CliConfiguration.cs +++ b/src/System.CommandLine/CliConfiguration.cs @@ -224,6 +224,7 @@ static CliSymbol GetChild(int index, CliCommand command, out AliasSet? aliases) */ public CliConfiguration(CliCommand command) { + RootCommand = command; } } } \ No newline at end of file From 44c0504123bf550dfca008d13dfd2a636398bf5f Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Mon, 4 Mar 2024 10:27:32 -0500 Subject: [PATCH 032/150] Reorg/rename in pipeline Made Initialize/Teardown subystems protected from public --- src/System.CommandLine.Subsystems/Pipeline.cs | 123 +++++++++++------- .../Subsystems/CliSubsystem.cs | 3 +- 2 files changed, 80 insertions(+), 46 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 00cc1c1dd4..b8250cdb5a 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// 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.Parsing; @@ -13,6 +13,32 @@ public class Pipeline public ErrorReportingSubsystem? ErrorReporting { get; set; } public CompletionSubsystem? Completion { get; set; } + public ParseResult Parse(CliConfiguration configuration, string rawInput) + => Parse(configuration, CliParser.SplitCommandLine(rawInput).ToArray()); + + public ParseResult Parse(CliConfiguration configuration, string[] args) + { + InitializeSubsystems(configuration); + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); + return parseResult; + } + + public CliExit Execute(CliConfiguration configuration, string rawInput, ConsoleHack? consoleHack = null) + => Execute(configuration, CliParser.SplitCommandLine(rawInput).ToArray(), rawInput, consoleHack); + + public CliExit Execute(CliConfiguration configuration, string[] args, string rawInput, ConsoleHack? consoleHack = null) + { + var cliExit = Execute(Parse(configuration, args), rawInput, consoleHack); + return TearDownSubsystems(cliExit); + } + + public CliExit Execute(ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) + { + var pipelineContext = new PipelineContext(parseResult, rawInput, this, consoleHack ?? new ConsoleHack()); + ExecuteSubsystems(pipelineContext); + return new CliExit(pipelineContext); + } + protected virtual void InitializeHelp(CliConfiguration configuration) => Help?.Initialize(configuration); @@ -22,20 +48,28 @@ protected virtual void InitializeVersion(CliConfiguration configuration) protected virtual void InitializeErrorReporting(CliConfiguration configuration) => ErrorReporting?.Initialize(configuration); - protected virtual void InitializeCompletions(CliConfiguration configuration) + protected virtual void InitializeCompletion(CliConfiguration configuration) => Completion?.Initialize(configuration); - protected virtual void TearDownHelp(PipelineContext context) - => Help?.TearDown(context); + protected virtual CliExit TearDownHelp(CliExit cliExit) + => Help is null + ? cliExit + : Help.TearDown(cliExit); - protected virtual void TearDownVersion(PipelineContext context) - => Version?.TearDown(context); + protected virtual CliExit? TearDownVersion(CliExit cliExit) + => Version is null + ? cliExit + : Version.TearDown(cliExit); - protected virtual void TearDownErrorReporting(PipelineContext context) - => ErrorReporting?.TearDown(context); + protected virtual CliExit TearDownErrorReporting(CliExit cliExit) + => ErrorReporting is null + ? cliExit + : ErrorReporting.TearDown(cliExit); - protected virtual void TearDownCompletions(PipelineContext context) - => Completion?.TearDown(context); + protected virtual CliExit TearDownCompletions(CliExit cliExit) + => Completion is null + ? cliExit + : Completion.TearDown(cliExit); protected virtual void ExecuteHelp(PipelineContext context) => ExecuteIfNeeded(Help, context); @@ -49,31 +83,44 @@ protected virtual void ExecuteErrorReporting(PipelineContext context) protected virtual void ExecuteCompletions(PipelineContext context) => ExecuteIfNeeded(Completion, context); - protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineContext pipelineContext) - { - if (subsystem is not null && (!pipelineContext.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) - { - subsystem.ExecuteIfNeeded(pipelineContext); - } - } - - public virtual void InitializeExtensions(CliConfiguration configuration) + // TODO: Consider whether this should be public. It would simplify testing, but would it do anything else + // TODO: Confirm that it is OK for ConsoleHack to be unavailable in Initialize + /// + /// Perform any setup for the subsystem. This may include adding to the CLI definition, + /// such as adding a help option. It is important that work only needed when the subsystem + /// + /// + /// + /// + /// Note to inheritors: The ordering of initializing should normally be in the reverse order than tear down + /// + protected virtual void InitializeSubsystems(CliConfiguration configuration) { InitializeHelp(configuration); InitializeVersion(configuration); InitializeErrorReporting(configuration); - InitializeCompletions(configuration); + InitializeCompletion(configuration); } - public virtual void TearDownExtensions(PipelineContext pipelineContext) + // TODO: Consider whether this should be public + // TODO: Would Dispose be a better alternative? This may be non-dispose like things, such as removing options? + /// + /// Perform any cleanup operations + /// + /// The context of the current execution + /// + /// Note to inheritors: The ordering of tear down should normally be in the reverse order than initializing + /// + protected virtual CliExit TearDownSubsystems(CliExit cliExit) { - TearDownHelp(pipelineContext); - TearDownVersion(pipelineContext); - TearDownErrorReporting(pipelineContext); - TearDownCompletions(pipelineContext); + TearDownCompletions(cliExit); + TearDownErrorReporting(cliExit); + TearDownVersion(cliExit); + TearDownHelp(cliExit); + return cliExit; } - protected virtual void ExecuteRequestedExtensions(PipelineContext pipelineContext) + protected virtual void ExecuteSubsystems(PipelineContext pipelineContext) { ExecuteHelp(pipelineContext); ExecuteVersion(pipelineContext); @@ -81,26 +128,12 @@ protected virtual void ExecuteRequestedExtensions(PipelineContext pipelineContex ExecuteCompletions(pipelineContext); } - public ParseResult Parse(CliConfiguration configuration, string rawInput) - => Parse(configuration, CliParser.SplitCommandLine(rawInput).ToArray()); - - public ParseResult Parse(CliConfiguration configuration, string[] args) + protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineContext pipelineContext) { - InitializeExtensions(configuration); - var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); - return parseResult; + if (subsystem is not null && (!pipelineContext.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) + { + subsystem.ExecuteIfNeeded(pipelineContext); + } } - public CliExit Execute(CliConfiguration configuration, string rawInput, ConsoleHack? consoleHack = null) - => Execute(configuration, CliParser.SplitCommandLine(rawInput).ToArray(), rawInput, consoleHack); - - public CliExit Execute(CliConfiguration configuration, string[] args, string rawInput, ConsoleHack? consoleHack = null) - => Execute(Parse(configuration, args), rawInput, consoleHack); - - public CliExit Execute(ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) - { - var pipelineContext = new PipelineContext(parseResult, rawInput, this, consoleHack ?? new ConsoleHack()); - ExecuteRequestedExtensions(pipelineContext); - return new CliExit(pipelineContext); - } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 6bd755f69e..794eff661d 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -118,6 +118,7 @@ internal PipelineContext ExecuteIfNeeded(ParseResult? parseResult, PipelineConte protected internal virtual CliConfiguration Initialize(CliConfiguration configuration) => configuration; // TODO: Determine if this is needed. - protected internal virtual PipelineContext TearDown(PipelineContext pipelineContext) => pipelineContext; + protected internal virtual CliExit TearDown(CliExit cliExit) + => cliExit; } From 6026408a78c539a2f561e8112ddb456a853e19dc Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Mon, 4 Mar 2024 11:54:14 -0500 Subject: [PATCH 033/150] Add Pipeline tests --- .../AlternateSubsystems.cs | 28 ++- .../Constants.cs | 19 ++ .../PipelineTests.cs | 191 ++++++++++++++++++ ...System.CommandLine.Subsystems.Tests.csproj | 2 + .../VersionSubsystemTests.cs | 22 +- .../Subsystems/Subsystem.cs | 4 + 6 files changed, 251 insertions(+), 15 deletions(-) create mode 100644 src/System.CommandLine.Subsystems.Tests/Constants.cs create mode 100644 src/System.CommandLine.Subsystems.Tests/PipelineTests.cs diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index 759e442954..9ae61750ac 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -7,7 +7,7 @@ namespace System.CommandLine.Subsystems.Tests { internal class AlternateSubsystems { - internal class Version : VersionSubsystem + internal class AlternateVersion : VersionSubsystem { protected override CliExit Execute(PipelineContext pipelineContext) { @@ -39,5 +39,31 @@ protected override CliExit Execute(PipelineContext pipelineContext) } } + internal class VersionWithInitializeAndTeardown : VersionSubsystem + { + internal bool InitializationWasRun; + internal bool ExecutionWasRun; + internal bool TeardownWasRun; + + protected override CliConfiguration Initialize(CliConfiguration configuration) + { + // marker hack needed because ConsoleHack not available in initialization + InitializationWasRun = true; + return base.Initialize(configuration); + } + + protected override CliExit Execute(PipelineContext pipelineContext) + { + ExecutionWasRun = true; + return base.Execute(pipelineContext); + } + + protected override CliExit TearDown(CliExit cliExit) + { + TeardownWasRun = true; + return base.TearDown(cliExit); + } + } + } } diff --git a/src/System.CommandLine.Subsystems.Tests/Constants.cs b/src/System.CommandLine.Subsystems.Tests/Constants.cs new file mode 100644 index 0000000000..3ad05c1614 --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/Constants.cs @@ -0,0 +1,19 @@ +// 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.Reflection; + +namespace System.CommandLine.Subsystems.Tests +{ + internal class Constants + { + + internal static readonly string? version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) + ?.GetCustomAttribute() + ?.InformationalVersion; + + internal static readonly string newLine = Environment.NewLine; + + + } +} diff --git a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs new file mode 100644 index 0000000000..297590108e --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs @@ -0,0 +1,191 @@ +// 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 FluentAssertions; +using System.CommandLine.Parsing; +using System.Reflection; +using Xunit; + +namespace System.CommandLine.Subsystems.Tests +{ + public class PipelineTests + { + + private static readonly string? version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) + ?.GetCustomAttribute() + ?.InformationalVersion; + + + [Theory] + [InlineData("-v", true)] + [InlineData("--version", true)] + [InlineData("-x", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void Subsystem_runs_in_pipeline_only_when_requested(string input, bool shouldRun) + { + var configuration = new CliConfiguration(new CliRootCommand { }); + var pipeline = new Pipeline + { + Version = new VersionSubsystem() + }; + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + + var exit = pipeline.Execute(configuration, input, consoleHack); + + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + if (shouldRun) + { + consoleHack.GetBuffer().Trim().Should().Be(version); + } + } + + [Theory] + [InlineData("-v", true)] + [InlineData("--version", true)] + [InlineData("-x", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void Subsystem_runs_with_explicit_parse_only_when_requested(string input, bool shouldRun) + { + var configuration = new CliConfiguration(new CliRootCommand { }); + var pipeline = new Pipeline + { + Version = new VersionSubsystem() + }; + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + + var result = pipeline.Parse(configuration, input); + var exit = pipeline.Execute(result, input, consoleHack); + + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + if (shouldRun) + { + consoleHack.GetBuffer().Trim().Should().Be(version); + } + } + + [Theory] + [InlineData("-v", true)] + [InlineData("--version", true)] + [InlineData("-x", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void Subsystem_runs_initialize_and_teardown_when_requested(string input, bool shouldRun) + { + var configuration = new CliConfiguration(new CliRootCommand { }); + AlternateSubsystems.VersionWithInitializeAndTeardown versionSubsystem = new AlternateSubsystems.VersionWithInitializeAndTeardown(); + var pipeline = new Pipeline + { + Version = versionSubsystem + }; + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + + var exit = pipeline.Execute(configuration, input, consoleHack); + + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + versionSubsystem.InitializationWasRun.Should().BeTrue(); + versionSubsystem.ExecutionWasRun.Should().Be(shouldRun); + versionSubsystem.TeardownWasRun.Should().BeTrue(); + } + + + [Theory] + [InlineData("-v", true)] + [InlineData("--version", true)] + [InlineData("-x", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void Subsystem_can_be_used_without_runner(string input, bool shouldRun) + { + var configuration = new CliConfiguration(new CliRootCommand { }); + var versionSubsystem = new VersionSubsystem(); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + + Subsystem.Initialize(versionSubsystem, configuration); + // TODO: I do not know why anyone would do this, but I do not see a reason to work to block it. See style2 below + var parseResult = CliParser.Parse(configuration.RootCommand, input, configuration); + bool value = parseResult.GetValue("--version"); + + value.Should().Be(shouldRun); + if (shouldRun) + { + // TODO: Add an execute overload to avoid checking activated twice + var exit = Subsystem.Execute(versionSubsystem, parseResult, input, consoleHack); + exit.Should().NotBeNull(); + exit.ExitCode.Should().Be(0); + exit.Handled.Should().BeTrue(); + consoleHack.GetBuffer().Trim().Should().Be(version); + } + } + + [Theory] + [InlineData("-v", true)] + [InlineData("--version", true)] + [InlineData("-x", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void Subsystem_can_be_used_without_runner_style2(string input, bool shouldRun) + { + var configuration = new CliConfiguration(new CliRootCommand { }); + var versionSubsystem = new VersionSubsystem(); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var expectedVersion = shouldRun + ? version + : ""; + + Subsystem.Initialize(versionSubsystem, configuration); + var parseResult = CliParser.Parse(configuration.RootCommand, input, configuration); + var exit = Subsystem.ExecuteIfNeeded(versionSubsystem, parseResult, input, consoleHack); + + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + consoleHack.GetBuffer().Trim().Should().Be(expectedVersion); + } + + [Fact] + public void Standard_pipeline_contains_expected_subsystems() + { + var pipeline = new StandardPipeline(); + pipeline.Version.Should().BeOfType(); + pipeline.Help.Should().BeOfType(); + pipeline.ErrorReporting.Should().BeOfType(); + pipeline.Completion.Should().BeOfType(); + } + + [Fact] + public void Normal_pipeline_contains_no_subsystems() + { + var pipeline = new Pipeline(); + pipeline.Version.Should().BeNull(); + pipeline.Help.Should().BeNull(); + pipeline.ErrorReporting.Should().BeNull(); + pipeline.Completion.Should().BeNull(); + } + + + [Fact] + public void Subsystems_can_access_each_others_data() + { + // TODO: Explore a mechanism that doesn't require the reference to retrieve data, this shows that it is awkward + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var symbol = new CliOption("-x"); + + var pipeline = new StandardPipeline + { + Version = new AlternateSubsystems.VersionThatUsesHelpData(symbol) + }; + if (pipeline.Help is null) throw new InvalidOperationException(); + var rootCommand = new CliRootCommand + { + symbol.With(pipeline.Help.Description, "Testing") + }; + pipeline.Execute(new CliConfiguration(rootCommand), "-v", consoleHack); + consoleHack.GetBuffer().Trim().Should().Be($"Testing"); + } + + } +} diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index 70f5d1432d..cf1c0f3704 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -25,6 +25,8 @@ + + diff --git a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs index 7af1769880..6cecb7beb9 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs @@ -8,15 +8,9 @@ namespace System.CommandLine.Subsystems.Tests { + public class VersionSubsystemTests { - private static readonly string? version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) - ?.GetCustomAttribute() - ?.InformationalVersion; - - private readonly string newLine = Environment.NewLine; - - [Fact] public void When_version_subsystem_is_used_the_version_option_is_added_to_the_root() { @@ -65,7 +59,7 @@ public void Outputs_assembly_version() var consoleHack = new ConsoleHack().RedirectToBuffer(true); var versionSubsystem = new VersionSubsystem(); Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be(version); + consoleHack.GetBuffer().Trim().Should().Be(Constants.version); } [Fact] @@ -89,7 +83,7 @@ public void Outputs_assembly_version_when_specified_version_set_to_null() SpecificVersion = null }; Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be(version); + consoleHack.GetBuffer().Trim().Should().Be(Constants.version); } [Fact] @@ -101,7 +95,7 @@ public void Console_output_can_be_tested() var consoleHack = new ConsoleHack().RedirectToBuffer(true); var versionSubsystem = new VersionSubsystem(); Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be(version); + consoleHack.GetBuffer().Trim().Should().Be(Constants.version); } [Fact] @@ -110,10 +104,10 @@ public void Custom_version_subsystem_can_be_used() var consoleHack = new ConsoleHack().RedirectToBuffer(true); var pipeline = new Pipeline { - Version = new AlternateSubsystems.Version() + Version = new AlternateSubsystems.AlternateVersion() }; pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); - consoleHack.GetBuffer().Trim().Should().Be($"***{version}***"); + consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); } [Fact] @@ -122,10 +116,10 @@ public void Custom_version_subsystem_can_replace_standard() var consoleHack = new ConsoleHack().RedirectToBuffer(true); var pipeline = new StandardPipeline { - Version = new AlternateSubsystems.Version() + Version = new AlternateSubsystems.AlternateVersion() }; pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); - consoleHack.GetBuffer().Trim().Should().Be($"***{version}***"); + consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); } } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs index 89295c0c7d..a61ab23ef2 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs @@ -17,6 +17,10 @@ public static bool GetIsActivated(CliSubsystem subsystem, ParseResult parseResul public static CliExit ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) => new(subsystem.ExecuteIfNeeded(new PipelineContext(parseResult, rawInput, null,consoleHack))); + public static CliExit Execute(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) + => subsystem.Execute(new PipelineContext(parseResult, rawInput, null, consoleHack)); + + internal static PipelineContext ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack, PipelineContext? pipelineContext = null) => subsystem.ExecuteIfNeeded(pipelineContext ?? new PipelineContext(parseResult, rawInput, null,consoleHack)); From fc074efdba6538fdb2a9d4d0005b4db3e9cd8039 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 5 Mar 2024 17:14:12 -0500 Subject: [PATCH 034/150] Store AnnotationsId prefix as separate field Avoiding the concat will marginally reduce the amount of heap memory, but more importantly makes it cleaner for annotation providers to check the prefix of the requested annotation ID. --- .../Subsystems/AnnotationId.cs | 5 ++++- .../Subsystems/Annotations/HelpAnnotations.cs | 6 +++--- .../Subsystems/Annotations/VersionAnnotations.cs | 6 +++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs b/src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs index 85bf9589b6..383214b42a 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs @@ -6,4 +6,7 @@ namespace System.CommandLine.Subsystems; /// /// Describes the ID and type of an annotation. /// -public record struct AnnotationId(string Id); +public record struct AnnotationId(string Prefix, string Id) +{ + public override readonly string ToString() => $"{Prefix}.{Id}"; +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs index 3d7eee1744..2dba0ab830 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs @@ -8,7 +8,7 @@ namespace System.CommandLine.Subsystems.Annotations; /// public static class HelpAnnotations { - // I made this public because we need an identifier for subsystem Kind, and I think this makes a very good one. - public static string Prefix { get; } = "Help."; - public static AnnotationId Description { get; } = new(Prefix + nameof(Description)); + public static string Prefix { get; } = nameof(SubsystemKind.Help); + + public static AnnotationId Description { get; } = new(Prefix, nameof(Description)); } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs index e8b411821a..463954e2da 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs @@ -8,7 +8,7 @@ namespace System.CommandLine.Subsystems.Annotations; /// public static class VersionAnnotations { - // I made this public because we need an identifier for subsystem Kind, and I think this makes a very good one. - public static string Prefix { get; } = "Version."; // TODO: Decide between const and property. Isn't a const baked in at the callsite? Would that be OK when this is public? - public static AnnotationId Version { get; } = new(Prefix + nameof(Version)); + public static string Prefix { get; } = nameof(SubsystemKind.Version); + + public static AnnotationId Version { get; } = new(Prefix, nameof(Version)); } From 303b5434ecc38abdf0d08ea74e78a62e94a0524b Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 5 Mar 2024 17:18:41 -0500 Subject: [PATCH 035/150] Stub Annotation classes for consistent use of Prefix --- .../CompletionSubsystem.cs | 3 ++- .../ErrorReportingSubsystem.cs | 3 ++- .../Subsystems/Annotations/CompletionAnnotations.cs | 12 ++++++++++++ .../Annotations/ErrorReportingAnnotations.cs | 12 ++++++++++++ .../VersionSubsystem.cs | 4 ++-- 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/CompletionAnnotations.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/ErrorReportingAnnotations.cs diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index 65ccdb4e16..bc024ca886 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -2,13 +2,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Subsystems; +using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine; public class CompletionSubsystem : CliSubsystem { public CompletionSubsystem(IAnnotationProvider? annotationProvider = null) - : base("Completion", annotationProvider, SubsystemKind.Completion) + : base(CompletionAnnotations.Prefix, annotationProvider, SubsystemKind.Completion) { } // TODO: Figure out trigger for completions diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs index 1a79e6eba8..9ded0c298e 100644 --- a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -2,13 +2,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Subsystems; +using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine; public class ErrorReportingSubsystem : CliSubsystem { public ErrorReportingSubsystem(IAnnotationProvider? annotationProvider = null) - : base("ErrorReporting", annotationProvider, SubsystemKind.ErrorReporting) + : base(ErrorReportingAnnotations.Prefix, annotationProvider, SubsystemKind.ErrorReporting) { } // TODO: Stash option rather than using string diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/CompletionAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/CompletionAnnotations.cs new file mode 100644 index 0000000000..7619d6687f --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/CompletionAnnotations.cs @@ -0,0 +1,12 @@ +// 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.Subsystems.Annotations; + +/// +/// IDs for well-known Completion annotations. +/// +public static class CompletionAnnotations +{ + public static string Prefix { get; } = nameof(SubsystemKind.Completion); +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ErrorReportingAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ErrorReportingAnnotations.cs new file mode 100644 index 0000000000..bb5dd594b2 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ErrorReportingAnnotations.cs @@ -0,0 +1,12 @@ +// 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.Subsystems.Annotations; + +/// +/// IDs for well-known ErrorReporting annotations. +/// +public static class ErrorReportingAnnotations +{ + public static string Prefix { get; } = nameof(SubsystemKind.ErrorReporting); +} diff --git a/src/System.CommandLine.Subsystems/VersionSubsystem.cs b/src/System.CommandLine.Subsystems/VersionSubsystem.cs index edec5c2eb8..0740e3bc6e 100644 --- a/src/System.CommandLine.Subsystems/VersionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/VersionSubsystem.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Subsystems; +using System.CommandLine.Subsystems.Annotations; using System.Reflection; namespace System.CommandLine; @@ -11,11 +12,10 @@ public class VersionSubsystem : CliSubsystem private string? specificVersion = null; public VersionSubsystem(IAnnotationProvider? annotationProvider = null) - : base("Version", annotationProvider, SubsystemKind.Version) + : base(VersionAnnotations.Prefix, annotationProvider, SubsystemKind.Version) { } - // TODO: Should we block adding version anywhere but root? public string? SpecificVersion { From 28617a3b198ec6f8e0bc03d287c1c70e5f8acbef Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Thu, 28 Mar 2024 14:54:00 -0400 Subject: [PATCH 036/150] Various changes to fix CI * Updated solution filter for source build * Comment out some failing tests * Fix some warnings * Get dotnet-suggest EndToEndApp compiling * Set nullable to annotations to allow ? without enforcing nullable rules * Skip tests that run only in release build because E2EApp not wired for completions --- System.CommandLine.sln | 15 +++++++++++++++ sourcebuild.slnf | 5 +---- .../System.CommandLine.Subsystems.Tests.csproj | 5 +++++ .../DotnetSuggestEndToEndTests.cs | 6 +++--- .../EndToEndTestApp/Program.cs | 17 +++++------------ src/System.CommandLine.Tests/ParserTests.cs | 9 +++++++++ .../System.CommandLine.Tests.csproj | 2 ++ src/System.CommandLine/CliArgument{T}.cs | 2 ++ src/System.CommandLine/CliCommand.cs | 2 ++ src/System.CommandLine/CliRootCommand.cs | 2 ++ .../Parsing/ArgumentResult.cs | 3 ++- src/System.CommandLine/Parsing/CliParser.cs | 7 +++++-- .../Parsing/ParseOperation.cs | 4 ++++ .../Parsing/StringExtensions.cs | 2 ++ 14 files changed, 59 insertions(+), 22 deletions(-) diff --git a/System.CommandLine.sln b/System.CommandLine.sln index ba72db885d..ea10b088e6 100644 --- a/System.CommandLine.sln +++ b/System.CommandLine.sln @@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Subsyste EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Subsystems.Tests", "src\System.CommandLine.Subsystems.Tests\System.CommandLine.Subsystems.Tests.csproj", "{7D6F74A4-28E4-4B57-8A4B-415A533729A7}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EndToEndTestApp", "src\System.CommandLine.Suggest.Tests\EndToEndTestApp\EndToEndTestApp.csproj", "{8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -161,6 +163,18 @@ Global {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Release|x64.Build.0 = Release|Any CPU {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Release|x86.ActiveCfg = Release|Any CPU {7D6F74A4-28E4-4B57-8A4B-415A533729A7}.Release|x86.Build.0 = Release|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Debug|x64.ActiveCfg = Debug|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Debug|x64.Build.0 = Debug|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Debug|x86.ActiveCfg = Debug|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Debug|x86.Build.0 = Debug|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Release|Any CPU.Build.0 = Release|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Release|x64.ActiveCfg = Release|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Release|x64.Build.0 = Release|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Release|x86.ActiveCfg = Release|Any CPU + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -175,6 +189,7 @@ Global {9E93F66A-6099-4675-AF53-FC10DE01925B} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {D750F504-DEBB-47B1-89AC-BB12B796E7B9} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} {7D6F74A4-28E4-4B57-8A4B-415A533729A7} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} + {8DAAEC0E-0B7C-4BEB-BD04-E197820E3568} = {E5B1EC71-0FC4-4FAA-9C65-32D5016FBC45} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5C159F93-800B-49E7-9905-EE09F8B8434A} diff --git a/sourcebuild.slnf b/sourcebuild.slnf index dc155a4d4e..22123b62b2 100644 --- a/sourcebuild.slnf +++ b/sourcebuild.slnf @@ -2,10 +2,7 @@ "solution": { "path": "System.CommandLine.sln", "projects": [ - "src\\System.CommandLine\\System.CommandLine.csproj", - "src\\System.CommandLine.DragonFruit\\System.CommandLine.DragonFruit.csproj", - "src\\System.CommandLine.NamingConventionBinder\\System.CommandLine.NamingConventionBinder.csproj", - "src\\System.CommandLine.Rendering\\System.CommandLine.Rendering.csproj" + "src\\System.CommandLine\\System.CommandLine.csproj" ] } } \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index cf1c0f3704..f93b8e94b9 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -6,7 +6,10 @@ $(DefaultExcludesInProjectFolder);TestApps\** Library enable + annotations + False @@ -23,7 +26,9 @@ --> + diff --git a/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs b/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs index 7e08fc8d65..2f34068045 100644 --- a/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs +++ b/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs @@ -71,7 +71,7 @@ private static void PrepareTestHomeDirectoryToAvoidPolluteBuildMachineHome() Directory.CreateDirectory(_testRoot); } - [ReleaseBuildOnlyFact] + [ReleaseBuildOnlyFact(Skip = "Temp E2EApp doesn't have completions wired up yet")] public void Test_app_supplies_suggestions() { var stdOut = new StringBuilder(); @@ -87,7 +87,7 @@ public void Test_app_supplies_suggestions() .Be($"--apple{NewLine}--banana{NewLine}--durian{NewLine}"); } - [ReleaseBuildOnlyFact] + [ReleaseBuildOnlyFact(Skip = "Temp E2EApp doesn't have completions wired up yet")] public void Dotnet_suggest_provides_suggestions_for_app() { // run "dotnet-suggest register" in explicit way @@ -122,7 +122,7 @@ public void Dotnet_suggest_provides_suggestions_for_app() .Be($"--apple{NewLine}--banana{NewLine}--durian{NewLine}"); } - [ReleaseBuildOnlyFact] + [ReleaseBuildOnlyFact(Skip ="Temp E2EApp doesn't have completions wired up yet")] public void Dotnet_suggest_provides_suggestions_for_app_with_only_commandname() { // run "dotnet-suggest register" in explicit way diff --git a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs index 637732152a..d5784546bc 100644 --- a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs +++ b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs @@ -1,5 +1,7 @@ using System.CommandLine; +/* using System.CommandLine.Help; +*/ using System.CommandLine.Parsing; using System.Threading.Tasks; using System.Threading; @@ -8,7 +10,7 @@ namespace EndToEndTestApp { public class Program { - static async Task Main(string[] args) + static void Main(string[] args) { CliOption appleOption = new ("--apple" ); CliOption bananaOption = new ("--banana"); @@ -23,19 +25,10 @@ static async Task Main(string[] args) durianOption, }; - rootCommand.SetAction((ParseResult ctx, CancellationToken cancellationToken) => - { - string apple = ctx.GetValue(appleOption); - string banana = ctx.GetValue(bananaOption); - string cherry = ctx.GetValue(cherryOption); - string durian = ctx.GetValue(durianOption); - - return Task.CompletedTask; - }); - CliConfiguration commandLine = new (rootCommand); - await commandLine.InvokeAsync(args); + var result = CliParser.Parse(commandLine.RootCommand, args, commandLine); + } } } diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 00024394ff..d2d93eab32 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -842,6 +842,8 @@ public void Absolute_Windows_style_paths_are_lexed_correctly() .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); } +// Default values +/* [Fact] public void Commands_can_have_default_argument_values() { @@ -960,6 +962,7 @@ public void Command_default_argument_value_does_not_override_parsed_value() .Should() .Be("the-directory"); } +*/ [Fact] public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_tokens() @@ -1479,6 +1482,8 @@ public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_tha new CliToken("5", CliTokenType.Argument, argument)); } +// TODO: Validation? +/* [Fact] public void When_command_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() { @@ -1517,6 +1522,7 @@ public void When_command_arguments_are_greater_than_maximum_arity_then_an_error_ .Should() .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); } +*/ [Fact] public void Option_argument_arity_can_be_a_fixed_value_greater_than_1() @@ -1568,6 +1574,8 @@ public void Option_argument_arity_can_be_a_range_with_a_lower_bound_greater_than new CliToken("5", CliTokenType.Argument, default)); } +// TODO: Validation? +/* [Fact] public void When_option_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() { @@ -1603,6 +1611,7 @@ public void When_option_arguments_are_greater_than_maximum_arity_then_an_error_i .Should() .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); } +*/ [Fact] public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_option() diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index 66b2d14b22..bdf2cd711e 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -17,7 +17,9 @@ + diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index b8d4adb0db..7a83a03dc1 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -27,11 +27,13 @@ public CliArgument(string name) : base(name) /// /// The delegate to invoke to create the default value. /// + /* /// /// It's invoked when there was no parse input provided for given Argument. /// The same instance can be set as , in such case /// the delegate is also invoked when an input was provided. /// + */ public Func? DefaultValueFactory { get; set; } // TODO: custom parsers diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index 0629ff293e..5749cae3b3 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -37,7 +37,9 @@ public class CliCommand : CliSymbol, IEnumerable /// Initializes a new instance of the Command class. /// /// The name of the command. + /* /// The description of the command, shown in help. + */ public CliCommand(string name)/*, string? description = null) */ : base(name) { diff --git a/src/System.CommandLine/CliRootCommand.cs b/src/System.CommandLine/CliRootCommand.cs index a5c472a4a3..b1cde62ddf 100644 --- a/src/System.CommandLine/CliRootCommand.cs +++ b/src/System.CommandLine/CliRootCommand.cs @@ -17,7 +17,9 @@ namespace System.CommandLine /// public class CliRootCommand : CliCommand { + /* /// The description of the command, shown in help. + */ public CliRootCommand(/*string description = "" */) : base(CliExecutable.ExecutableName/*, description*/) { diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/ArgumentResult.cs index 561ce36a02..49fedd4667 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/ArgumentResult.cs @@ -41,8 +41,9 @@ public T GetValueOrDefault() => .ConvertIfNeeded(typeof(T)) .GetValueOrDefault(); + // TODO: Fix cref for unmatched tokens /// - /// Specifies the maximum number of tokens to consume for the argument. Remaining tokens are passed on and can be consumed by later arguments, or will otherwise be added to + /// Specifies the maximum number of tokens to consume for the argument. Remaining tokens are passed on and can be consumed by later arguments, or will otherwise be added to see cref="ParseResult.UnmatchedTokens"/> /// /// The number of tokens to take. The rest are passed on. /// numberOfTokens - Value must be at least 1. diff --git a/src/System.CommandLine/Parsing/CliParser.cs b/src/System.CommandLine/Parsing/CliParser.cs index fc159df716..0b5fd199c0 100644 --- a/src/System.CommandLine/Parsing/CliParser.cs +++ b/src/System.CommandLine/Parsing/CliParser.cs @@ -11,10 +11,13 @@ namespace System.CommandLine.Parsing /// public static class CliParser { + /* + /// The command to use to parse the command line input. + */ /// /// Parses a list of arguments. /// - /// The command to use to parse the command line input. + /// /// The string array typically passed to a program's Main method. /// The configuration on which the parser's grammar and behaviors are based. /// A providing details about the parse operation. @@ -24,7 +27,7 @@ public static ParseResult Parse(CliCommand rootCommand, IReadOnlyList ar /// /// Parses a command line string. /// - /// The command to use to parse the command line input. + /// The command to use to parse the command line input. /// The complete command line input prior to splitting and tokenization. This input is not typically available when the parser is called from Program.Main. It is primarily used when calculating completions via the dotnet-suggest tool. /// The configuration on which the parser's grammar and behaviors are based. /// The command line string input will be split into tokens as if it had been passed on the command line. diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 1bbf6fb0c1..56d6373ff0 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -15,8 +15,10 @@ internal sealed class ParseOperation private int _index; private CommandResult _innermostCommandResult; + /* private bool _isHelpRequested; private bool _isTerminatingDirectiveSpecified; + */ // TODO: invocation /* private CliAction? _primaryAction; @@ -61,10 +63,12 @@ internal ParseResult Parse() ParseDirectives(); */ ParseCommandChildren(); + /* if (!_isHelpRequested) { Validate(); } + */ // TODO: invocation /* diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index f4710a9385..bee1ab5ad1 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -64,7 +64,9 @@ internal static void Tokenize( var currentCommand = rootCommand; var foundDoubleDash = false; // TODO: Directives + /* var foundEndOfDirectives = false; + */ var tokenList = new List(args.Count); From 1aca22a0350e800b72f2275df12a58f0a1f72101 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 2 Apr 2024 14:02:46 -0400 Subject: [PATCH 037/150] Revert bad autoformatter changes from 49e43932 --- src/System.CommandLine.Tests/ParserTests.cs | 2652 +++++++++---------- 1 file changed, 1326 insertions(+), 1326 deletions(-) diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index d2d93eab32..8d37106b40 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -63,9 +63,9 @@ public void When_a_token_is_just_a_prefix_then_an_error_is_returned(string prefi var result = CliParser.Parse(rootCommand, prefix); result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument(prefix)); + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument(prefix)); } [Fact] @@ -94,1588 +94,1588 @@ public void Long_form_options_can_be_specified_using_equals_delimiter() result.GetResult(option).Tokens.Should().ContainSingle(a => a.Value == "there"); } - [Fact] - public void Short_form_options_can_be_specified_using_colon_delimiter() - { - var option = new CliOption("-x"); + [Fact] + public void Short_form_options_can_be_specified_using_colon_delimiter() + { + var option = new CliOption("-x"); var rootCommand = new CliRootCommand { option }; var result = CliParser.Parse(rootCommand,"-x:some-value"); - result.Errors.Should().BeEmpty(); + result.Errors.Should().BeEmpty(); - result.GetResult(option).Tokens.Should().ContainSingle(a => a.Value == "some-value"); - } + result.GetResult(option).Tokens.Should().ContainSingle(a => a.Value == "some-value"); + } - [Fact] - public void Long_form_options_can_be_specified_using_colon_delimiter() - { - var option = new CliOption("--hello"); + [Fact] + public void Long_form_options_can_be_specified_using_colon_delimiter() + { + var option = new CliOption("--hello"); var rootCommand = new CliRootCommand { option }; var result = CliParser.Parse(rootCommand,"--hello:there"); - result.Errors.Should().BeEmpty(); - - result.GetResult(option).Tokens.Should().ContainSingle(a => a.Value == "there"); - } + result.Errors.Should().BeEmpty(); - [Fact] - public void Option_short_forms_can_be_bundled() - { - var command = new CliCommand("the-command"); - command.Options.Add(new CliOption("-x")); - command.Options.Add(new CliOption("-y")); - command.Options.Add(new CliOption("-z")); - - var result = CliParser.Parse(command, "the-command -xyz"); - - result.CommandResult - .Children - .Select(o => ((OptionResult)o).Option.Name) - .Should() - .BeEquivalentTo("-x", "-y", "-z"); - } + result.GetResult(option).Tokens.Should().ContainSingle(a => a.Value == "there"); + } - /* + [Fact] + public void Option_short_forms_can_be_bundled() + { + var command = new CliCommand("the-command"); + command.Options.Add(new CliOption("-x")); + command.Options.Add(new CliOption("-y")); + command.Options.Add(new CliOption("-z")); - [Fact] - public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off() - { - // TODO: umatched tokens has been moved, fix - CliRootCommand rootCommand = new CliRootCommand() - { - new CliCommand("the-command") - { - new CliOption("-x"), - new CliOption("-y"), - new CliOption("-z") - } - }; - - CliConfiguration configuration = new (rootCommand) - { - EnablePosixBundling = false - }; + var result = CliParser.Parse(command, "the-command -xyz"); - var result = rootCommand.Parse("the-command -xyz", configuration); + result.CommandResult + .Children + .Select(o => ((OptionResult)o).Option.Name) + .Should() + .BeEquivalentTo("-x", "-y", "-z"); + } - result.UnmatchedTokens - .Should() - .BeEquivalentTo("-xyz"); - } - */ + /* - [Fact] - public void Option_long_forms_do_not_get_unbundled() + [Fact] + public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off() + { + // TODO: umatched tokens has been moved, fix + CliRootCommand rootCommand = new CliRootCommand() + { + new CliCommand("the-command") { - CliCommand command = - new CliCommand("the-command") - { - new CliOption("--xyz"), - new CliOption("-x"), - new CliOption("-y"), - new CliOption("-z") - }; - - var result = CliParser.Parse(command, "the-command --xyz"); - - result.CommandResult - .Children - .Select(o => ((OptionResult)o).Option.Name) - .Should() - .BeEquivalentTo("--xyz"); + new CliOption("-x"), + new CliOption("-y"), + new CliOption("-z") } + }; - [Fact] - public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_valid_for_the_current_command() - { - var outer = new CliCommand("outer"); - outer.Options.Add(new CliOption("-a")); - var inner = new CliCommand("inner") - { - new CliArgument("arg") - }; - inner.Options.Add(new CliOption("-b")); - inner.Options.Add(new CliOption("-c")); - outer.Subcommands.Add(inner); - - ParseResult result = CliParser.Parse(outer, "outer inner -abc"); - - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("-abc"); - } + CliConfiguration configuration = new (rootCommand) + { + EnablePosixBundling = false + }; - [Fact] - public void Required_option_arguments_are_not_unbundled() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b"); - var optionC = new CliOption("-c"); + var result = rootCommand.Parse("the-command -xyz", configuration); - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + result.UnmatchedTokens + .Should() + .BeEquivalentTo("-xyz"); + } + */ - var result = CliParser.Parse(command, "-a -bc"); + [Fact] + public void Option_long_forms_do_not_get_unbundled() + { + CliCommand command = + new CliCommand("the-command") + { + new CliOption("--xyz"), + new CliOption("-x"), + new CliOption("-y"), + new CliOption("-z") + }; - result.GetResult(optionA) - .Tokens - .Should() - .ContainSingle(t => t.Value == "-bc"); - } + var result = CliParser.Parse(command, "the-command --xyz"); - [Fact] - public void Last_bundled_option_can_accept_argument_with_no_separator() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; - var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; + result.CommandResult + .Children + .Select(o => ((OptionResult)o).Option.Name) + .Should() + .BeEquivalentTo("--xyz"); + } - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; - - var result = CliParser.Parse(command, "-abcvalue"); - result.GetResult(optionA).Should().NotBeNull(); - result.GetResult(optionB).Should().NotBeNull(); - - result.GetResult(optionC) - .Tokens - .Should() - .ContainSingle(t => t.Value == "value"); - } + [Fact] + public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_valid_for_the_current_command() + { + var outer = new CliCommand("outer"); + outer.Options.Add(new CliOption("-a")); + var inner = new CliCommand("inner") + { + new CliArgument("arg") + }; + inner.Options.Add(new CliOption("-b")); + inner.Options.Add(new CliOption("-c")); + outer.Subcommands.Add(inner); + + ParseResult result = CliParser.Parse(outer, "outer inner -abc"); + + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("-abc"); + } - [Fact] - public void Last_bundled_option_can_accept_argument_with_equals_separator() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; - var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; + [Fact] + public void Required_option_arguments_are_not_unbundled() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b"); + var optionC = new CliOption("-c"); - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; - - var result = CliParser.Parse(command, "-abc=value"); - result.GetResult(optionA).Should().NotBeNull(); - result.GetResult(optionB).Should().NotBeNull(); - - result.GetResult(optionC) - .Tokens - .Should() - .ContainSingle(t => t.Value == "value"); - } + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; - [Fact] - public void Last_bundled_option_can_accept_argument_with_colon_separator() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; - var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; + var result = CliParser.Parse(command, "-a -bc"); - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; - - var result = CliParser.Parse(command, "-abc:value"); - result.GetResult(optionA).Should().NotBeNull(); - result.GetResult(optionB).Should().NotBeNull(); - - result.GetResult(optionC) - .Tokens - .Should() - .ContainSingle(t => t.Value == "value"); - } + result.GetResult(optionA) + .Tokens + .Should() + .ContainSingle(t => t.Value == "-bc"); + } - [Fact] - public void Invalid_char_in_bundle_causes_rest_to_be_interpreted_as_value() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; - var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; + [Fact] + public void Last_bundled_option_can_accept_argument_with_no_separator() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; + var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; + + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; + + var result = CliParser.Parse(command, "-abcvalue"); + result.GetResult(optionA).Should().NotBeNull(); + result.GetResult(optionB).Should().NotBeNull(); + + result.GetResult(optionC) + .Tokens + .Should() + .ContainSingle(t => t.Value == "value"); + } - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + [Fact] + public void Last_bundled_option_can_accept_argument_with_equals_separator() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; + var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; + + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; + + var result = CliParser.Parse(command, "-abc=value"); + result.GetResult(optionA).Should().NotBeNull(); + result.GetResult(optionB).Should().NotBeNull(); + + result.GetResult(optionC) + .Tokens + .Should() + .ContainSingle(t => t.Value == "value"); + } - var result = CliParser.Parse(command, "-abvcalue"); - result.GetResult(optionA).Should().NotBeNull(); - result.GetResult(optionB).Should().NotBeNull(); + [Fact] + public void Last_bundled_option_can_accept_argument_with_colon_separator() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; + var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; + + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; + + var result = CliParser.Parse(command, "-abc:value"); + result.GetResult(optionA).Should().NotBeNull(); + result.GetResult(optionB).Should().NotBeNull(); + + result.GetResult(optionC) + .Tokens + .Should() + .ContainSingle(t => t.Value == "value"); + } - result.GetResult(optionB) - .Tokens - .Should() - .ContainSingle(t => t.Value == "vcalue"); + [Fact] + public void Invalid_char_in_bundle_causes_rest_to_be_interpreted_as_value() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b") { Arity = ArgumentArity.ZeroOrOne }; + var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; + + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; + + var result = CliParser.Parse(command, "-abvcalue"); + result.GetResult(optionA).Should().NotBeNull(); + result.GetResult(optionB).Should().NotBeNull(); + + result.GetResult(optionB) + .Tokens + .Should() + .ContainSingle(t => t.Value == "vcalue"); - result.GetResult(optionC).Should().BeNull(); - } + result.GetResult(optionC).Should().BeNull(); + } - [Fact] - public void Parser_root_Options_can_be_specified_multiple_times_and_their_arguments_are_collated() - { - var animalsOption = new CliOption("-a", "--animals"); - var vegetablesOption = new CliOption("-v", "--vegetables"); - var rootCommand = new CliRootCommand - { - animalsOption, - vegetablesOption - }; - - var result = CliParser.Parse(rootCommand, "-a cat -v carrot -a dog"); - - result.GetResult(animalsOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("cat", "dog"); - - result.GetResult(vegetablesOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("carrot"); - } + [Fact] + public void Parser_root_Options_can_be_specified_multiple_times_and_their_arguments_are_collated() + { + var animalsOption = new CliOption("-a", "--animals"); + var vegetablesOption = new CliOption("-v", "--vegetables"); + var rootCommand = new CliRootCommand + { + animalsOption, + vegetablesOption + }; + + var result = CliParser.Parse(rootCommand, "-a cat -v carrot -a dog"); + + result.GetResult(animalsOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("cat", "dog"); + + result.GetResult(vegetablesOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("carrot"); + } /* - [Fact] - public void Options_can_be_specified_multiple_times_and_their_arguments_are_collated() - { + [Fact] + public void Options_can_be_specified_multiple_times_and_their_arguments_are_collated() + { // TODO: tests AcceptOnlyFromAmong, fix - var animalsOption = new CliOption("-a", "--animals"); - animalsOption.AcceptOnlyFromAmong("dog", "cat", "sheep"); - var vegetablesOption = new CliOption("-v", "--vegetables"); - CliCommand command = - new CliCommand("the-command") { - animalsOption, - vegetablesOption - }; - - var result = command.Parse("the-command -a cat -v carrot -a dog"); - - result.GetResult(animalsOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("cat", "dog"); - - result.GetResult(vegetablesOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("carrot"); - } + var animalsOption = new CliOption("-a", "--animals"); + animalsOption.AcceptOnlyFromAmong("dog", "cat", "sheep"); + var vegetablesOption = new CliOption("-v", "--vegetables"); + CliCommand command = + new CliCommand("the-command") { + animalsOption, + vegetablesOption + }; + + var result = command.Parse("the-command -a cat -v carrot -a dog"); + + result.GetResult(animalsOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("cat", "dog"); + + result.GetResult(vegetablesOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("carrot"); + } */ - [Fact] - public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_following_token_is_considered_an_argument_to_the_parent_command() - { - var animalsOption = new CliOption("-a", "--animals"); - - var vegetablesOption = new CliOption("-v", "--vegetables"); - - CliCommand command = - new CliCommand("the-command") - { - animalsOption, - vegetablesOption, - new CliArgument("arg") - }; - - var result = CliParser.Parse(command, "the-command -a cat some-arg -v carrot"); - - result.GetResult(animalsOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("cat"); - - result.GetResult(vegetablesOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("carrot"); - - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("some-arg"); - } + [Fact] + public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_following_token_is_considered_an_argument_to_the_parent_command() + { + var animalsOption = new CliOption("-a", "--animals"); - [Fact] - public void Command_with_multiple_options_is_parsed_correctly() - { - var command = new CliCommand("outer") - { - new CliOption("--inner1"), - new CliOption("--inner2") - }; - - var result = CliParser.Parse(command, "outer --inner1 argument1 --inner2 argument2"); - - result.CommandResult - .Children - .Should() - .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner1" && - o.Tokens.Single().Value == "argument1"); - result.CommandResult - .Children - .Should() - .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner2" && - o.Tokens.Single().Value == "argument2"); - } + var vegetablesOption = new CliOption("-v", "--vegetables"); - [Fact] - public void Relative_order_of_arguments_and_options_within_a_command_does_not_matter() + CliCommand command = + new CliCommand("the-command") { - var command = new CliCommand("move") - { - new CliArgument("arg"), - new CliOption("-X") - }; - - // option before args - ParseResult result1 = CliParser.Parse( - command, - "move -X the-arg-for-option-x ARG1 ARG2"); - - // option between two args - ParseResult result2 = CliParser.Parse( - command, - "move ARG1 -X the-arg-for-option-x ARG2"); - - // option after args - ParseResult result3 = CliParser.Parse( - command, - "move ARG1 ARG2 -X the-arg-for-option-x"); - - // all should be equivalent - result1.Should() - .BeEquivalentTo( - result2, - x => x.IgnoringCyclicReferences() - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); - result1.Should() - .BeEquivalentTo( - result3, - x => x.IgnoringCyclicReferences() - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); - } + animalsOption, + vegetablesOption, + new CliArgument("arg") + }; - [Theory] - [InlineData("--one 1 --many 1 --many 2")] - [InlineData("--one 1 --many 1 --many 2 arg1 arg2")] - [InlineData("--many 1 --one 1 --many 2")] - [InlineData("--many 2 --many 1 --one 1")] - [InlineData("[parse] --one 1 --many 1 --many 2")] - [InlineData("--one \"stuff in quotes\" this-is-arg1 \"this is arg2\"")] - [InlineData("not a valid command line --one 1")] - public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string commandLine) - { - var rawSplit = CliParser.SplitCommandLine(commandLine); + var result = CliParser.Parse(command, "the-command -a cat some-arg -v carrot"); - var command = new CliCommand("the-command") - { - new CliArgument("arg"), - new CliOption("--one"), - new CliOption("--many") - }; + result.GetResult(animalsOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("cat"); - var result = CliParser.Parse(command, commandLine); + result.GetResult(vegetablesOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("carrot"); - result.Tokens.Select(t => t.Value).Should().Equal(rawSplit); - } + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("some-arg"); + } + + [Fact] + public void Command_with_multiple_options_is_parsed_correctly() + { + var command = new CliCommand("outer") + { + new CliOption("--inner1"), + new CliOption("--inner2") + }; + + var result = CliParser.Parse(command, "outer --inner1 argument1 --inner2 argument2"); + + result.CommandResult + .Children + .Should() + .ContainSingle(o => + ((OptionResult)o).Option.Name == "--inner1" && + o.Tokens.Single().Value == "argument1"); + result.CommandResult + .Children + .Should() + .ContainSingle(o => + ((OptionResult)o).Option.Name == "--inner2" && + o.Tokens.Single().Value == "argument2"); + } + + [Fact] + public void Relative_order_of_arguments_and_options_within_a_command_does_not_matter() + { + var command = new CliCommand("move") + { + new CliArgument("arg"), + new CliOption("-X") + }; + + // option before args + ParseResult result1 = CliParser.Parse( + command, + "move -X the-arg-for-option-x ARG1 ARG2"); + + // option between two args + ParseResult result2 = CliParser.Parse( + command, + "move ARG1 -X the-arg-for-option-x ARG2"); + + // option after args + ParseResult result3 = CliParser.Parse( + command, + "move ARG1 ARG2 -X the-arg-for-option-x"); + + // all should be equivalent + result1.Should() + .BeEquivalentTo( + result2, + x => x.IgnoringCyclicReferences() + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); + result1.Should() + .BeEquivalentTo( + result3, + x => x.IgnoringCyclicReferences() + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); + } + + [Theory] + [InlineData("--one 1 --many 1 --many 2")] + [InlineData("--one 1 --many 1 --many 2 arg1 arg2")] + [InlineData("--many 1 --one 1 --many 2")] + [InlineData("--many 2 --many 1 --one 1")] + [InlineData("[parse] --one 1 --many 1 --many 2")] + [InlineData("--one \"stuff in quotes\" this-is-arg1 \"this is arg2\"")] + [InlineData("not a valid command line --one 1")] + public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string commandLine) + { + var rawSplit = CliParser.SplitCommandLine(commandLine); + + var command = new CliCommand("the-command") + { + new CliArgument("arg"), + new CliOption("--one"), + new CliOption("--many") + }; + + var result = CliParser.Parse(command, commandLine); + + result.Tokens.Select(t => t.Value).Should().Equal(rawSplit); + } /* - [Fact] - public void An_outer_command_with_the_same_name_does_not_capture() + [Fact] + public void An_outer_command_with_the_same_name_does_not_capture() + { + // TODO: uses Diagram, fix + var command = new CliCommand("one") + { + new CliCommand("two") { - // TODO: uses Diagram, fix - var command = new CliCommand("one") - { - new CliCommand("two") - { - new CliCommand("three") - }, - new CliCommand("three") - }; - - ParseResult result = CliParser.Parse(command, "one two three"); - - result.Diagram().Should().Be("[ one [ two [ three ] ] ]"); - } + new CliCommand("three") + }, + new CliCommand("three") + }; + + ParseResult result = CliParser.Parse(command, "one two three"); - [Fact] - public void An_inner_command_with_the_same_name_does_not_capture() + result.Diagram().Should().Be("[ one [ two [ three ] ] ]"); + } + + [Fact] + public void An_inner_command_with_the_same_name_does_not_capture() + { + // TODO: uses Diagram, fix + var command = new CliCommand("one") + { + new CliCommand("two") { - // TODO: uses Diagram, fix - var command = new CliCommand("one") - { - new CliCommand("two") - { - new CliCommand("three") - }, - new CliCommand("three") - }; - - ParseResult result = CliParser.Parse(command, "one three"); - - result.Diagram().Should().Be("[ one [ three ] ]"); - } + new CliCommand("three") + }, + new CliCommand("three") + }; + + ParseResult result = CliParser.Parse(command, "one three"); + + result.Diagram().Should().Be("[ one [ three ] ]"); + } */ - [Fact] - public void When_nested_commands_all_accept_arguments_then_the_nearest_captures_the_arguments() + [Fact] + public void When_nested_commands_all_accept_arguments_then_the_nearest_captures_the_arguments() + { + var command = new CliCommand("outer") + { + new CliArgument("arg1"), + new CliCommand("inner") { - var command = new CliCommand( - "outer") - { - new CliArgument("arg1"), - new CliCommand("inner") - { - new CliArgument("arg2") - } - }; - - var result = CliParser.Parse(command, "outer arg1 inner arg2"); - - result.CommandResult - .Parent - .Tokens.Select(t => t.Value) - .Should() - .BeEquivalentTo("arg1"); - - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("arg2"); + new CliArgument("arg2") } + }; + + var result = CliParser.Parse(command, "outer arg1 inner arg2"); + + result.CommandResult + .Parent + .Tokens.Select(t => t.Value) + .Should() + .BeEquivalentTo("arg1"); + + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("arg2"); + } /* - [Fact] - public void Nested_commands_with_colliding_names_cannot_both_be_applied() + [Fact] + public void Nested_commands_with_colliding_names_cannot_both_be_applied() + { + // TODO: uses Diagram, fix + var command = new CliCommand("outer") + { + new CliArgument("arg1"), + new CliCommand("non-unique") + { + new CliArgument("arg2") + }, + new CliCommand("inner") { - // TODO: uses Diagram, fix - var command = new CliCommand("outer") + new CliArgument("arg3"), + new CliCommand("non-unique") { - new CliArgument("arg1"), - new CliCommand("non-unique") - { - new CliArgument("arg2") - }, - new CliCommand("inner") - { - new CliArgument("arg3"), - new CliCommand("non-unique") - { - new CliArgument("arg4") - } - } - }; - - ParseResult result = command.Parse("outer arg1 inner arg2 non-unique arg3 "); - - result.Diagram().Should().Be("[ outer [ inner [ non-unique ] ] ]"); + new CliArgument("arg4") + } } + }; + + ParseResult result = command.Parse("outer arg1 inner arg2 non-unique arg3 "); + + result.Diagram().Should().Be("[ outer [ inner [ non-unique ] ] ]"); + } */ - [Fact] - public void When_child_option_will_not_accept_arg_then_parent_can() - { - var option = new CliOption("-x"); - var command = new CliCommand("the-command") - { - option, - new CliArgument("arg") - }; - - var result = CliParser.Parse(command, "the-command -x the-argument"); - - var optionResult = result.GetResult(option); - optionResult.Tokens.Should().BeEmpty(); - result.CommandResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); - } + [Fact] + public void When_child_option_will_not_accept_arg_then_parent_can() + { + var option = new CliOption("-x"); + var command = new CliCommand("the-command") + { + option, + new CliArgument("arg") + }; + + var result = CliParser.Parse(command, "the-command -x the-argument"); + + var optionResult = result.GetResult(option); + optionResult.Tokens.Should().BeEmpty(); + result.CommandResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); + } - [Fact] - public void When_parent_option_will_not_accept_arg_then_child_can() - { - var option = new CliOption("-x"); - var command = new CliCommand("the-command") - { - option - }; + [Fact] + public void When_parent_option_will_not_accept_arg_then_child_can() + { + var option = new CliOption("-x"); + var command = new CliCommand("the-command") + { + option + }; - var result = CliParser.Parse(command, "the-command -x the-argument"); + var result = CliParser.Parse(command, "the-command -x the-argument"); - result.GetResult(option).Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); - result.CommandResult.Tokens.Should().BeEmpty(); - } + result.GetResult(option).Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); + result.CommandResult.Tokens.Should().BeEmpty(); + } - [Fact] - public void Required_arguments_on_parent_commands_do_not_create_parse_errors_when_an_inner_command_is_specified() - { - var child = new CliCommand("child"); + [Fact] + public void Required_arguments_on_parent_commands_do_not_create_parse_errors_when_an_inner_command_is_specified() + { + var child = new CliCommand("child"); - var parent = new CliCommand("parent") - { - new CliArgument("arg"), - child - }; + var parent = new CliCommand("parent") + { + new CliArgument("arg"), + child + }; - var result = CliParser.Parse(parent, "child"); + var result = CliParser.Parse(parent, "child"); - result.Errors.Should().BeEmpty(); + result.Errors.Should().BeEmpty(); + } + + [Fact] + public void Required_arguments_on_grandparent_commands_do_not_create_parse_errors_when_an_inner_command_is_specified() + { + var grandchild = new CliCommand("grandchild"); + + var grandparent = new CliCommand("grandparent") + { + new CliArgument("arg"), + new CliCommand("parent") + { + grandchild } + }; + + var result = CliParser.Parse(grandparent, "parent grandchild"); + + result.Errors.Should().BeEmpty(); + } - [Fact] - public void Required_arguments_on_grandparent_commands_do_not_create_parse_errors_when_an_inner_command_is_specified() + [Fact] + public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_at_the_end_then_it_attaches_to_the_inner_command() + { + var outer = new CliCommand("outer") + { + new CliCommand("inner") { - var grandchild = new CliCommand("grandchild"); + new CliOption("-x") + }, + new CliOption("-x") + }; - var grandparent = new CliCommand("grandparent") - { - new CliArgument("arg"), - new CliCommand("parent") - { - grandchild - } - }; + ParseResult result = CliParser.Parse(outer, "outer inner -x"); - var result = CliParser.Parse(grandparent, "parent grandchild"); + result.CommandResult + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .AllBeAssignableTo(); + result.CommandResult + .Children + .Should() + .ContainSingle(o => ((OptionResult)o).Option.Name == "-x"); + } - result.Errors.Should().BeEmpty(); - } + [Fact] + public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_in_between_then_it_attaches_to_the_outer_command() + { + var outer = new CliCommand("outer"); + outer.Options.Add(new CliOption("-x")); + var inner = new CliCommand("inner"); + inner.Options.Add(new CliOption("-x")); + outer.Subcommands.Add(inner); - [Fact] - public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_at_the_end_then_it_attaches_to_the_inner_command() - { - var outer = new CliCommand("outer") - { - new CliCommand("inner") - { - new CliOption("-x") - }, - new CliOption("-x") - }; - - ParseResult result = CliParser.Parse(outer, "outer inner -x"); - - result.CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .AllBeAssignableTo(); - result.CommandResult - .Children - .Should() - .ContainSingle(o => ((OptionResult)o).Option.Name == "-x"); - } + var result = CliParser.Parse(outer, "outer -x inner"); - [Fact] - public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_in_between_then_it_attaches_to_the_outer_command() - { - var outer = new CliCommand("outer"); - outer.Options.Add(new CliOption("-x")); - var inner = new CliCommand("inner"); - inner.Options.Add(new CliOption("-x")); - outer.Subcommands.Add(inner); - - var result = CliParser.Parse(outer, "outer -x inner"); - - result.CommandResult - .Children - .Should() - .BeEmpty(); - result.CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); - } + result.CommandResult + .Children + .Should() + .BeEmpty(); + result.CommandResult + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); + } /* - [Fact] - // TODO: tests unmatched tokens, needs fix - public void Arguments_only_apply_to_the_nearest_command() + [Fact] + // TODO: tests unmatched tokens, needs fix + public void Arguments_only_apply_to_the_nearest_command() + { + var outer = new CliCommand("outer") + { + new CliArgument("arg1"), + new CliCommand("inner") { - var outer = new CliCommand("outer") - { - new CliArgument("arg1"), - new CliCommand("inner") - { - new CliArgument("arg2") - } - }; - - ParseResult result = outer.Parse("outer inner arg1 arg2"); - - result.CommandResult - .Parent - .Tokens - .Should() - .BeEmpty(); - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("arg1"); - result.UnmatchedTokens - .Should() - .BeEquivalentTo("arg2"); + new CliArgument("arg2") } + }; + + ParseResult result = outer.Parse("outer inner arg1 arg2"); + + result.CommandResult + .Parent + .Tokens + .Should() + .BeEmpty(); + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("arg1"); + result.UnmatchedTokens + .Should() + .BeEquivalentTo("arg2"); + } */ - [Fact] - public void Options_only_apply_to_the_nearest_command() + [Fact] + public void Options_only_apply_to_the_nearest_command() + { + var outerOption = new CliOption("-x"); + var innerOption = new CliOption("-x"); + + var outer = new CliCommand("outer") + { + new CliCommand("inner") { - var outerOption = new CliOption("-x"); - var innerOption = new CliOption("-x"); - - var outer = new CliCommand("outer") - { - new CliCommand("inner") - { - innerOption - }, - outerOption - }; - - var result = CliParser.Parse(outer, "outer inner -x one -x two"); - - result.RootCommandResult - .GetResult(outerOption) - .Should() - .BeNull(); - } + innerOption + }, + outerOption + }; + + var result = CliParser.Parse(outer, "outer inner -x one -x two"); + + result.RootCommandResult + .GetResult(outerOption) + .Should() + .BeNull(); + } - [Fact] - public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_as_arguments() + [Fact] + public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_as_arguments() + { + var command = new CliCommand("the-command") + { + new CliCommand("complete") { - var command = new CliCommand("the-command") - { - new CliCommand("complete") - { - new CliArgument("arg"), - new CliOption("--position") - } - }; - - ParseResult result = CliParser.Parse(command, new[] { "the-command", - "complete", - "--position", - "7", - "the-command" }); - - CommandResult completeResult = result.CommandResult; - - completeResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-command"); + new CliArgument("arg"), + new CliOption("--position") } + }; - [Fact] - public void Absolute_unix_style_paths_are_lexed_correctly() - { - const string commandText = - @"rm ""/temp/the file.txt"""; + ParseResult result = CliParser.Parse( + command, new[] { + "the-command", + "complete", + "--position", + "7", + "the-command" + }); - CliCommand command = new ("rm") - { - new CliArgument("arg") - }; + CommandResult completeResult = result.CommandResult; - var result = CliParser.Parse(command, commandText); + completeResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-command"); + } - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .OnlyContain(a => a == @"/temp/the file.txt"); - } + [Fact] + public void Absolute_unix_style_paths_are_lexed_correctly() + { + const string commandText = + @"rm ""/temp/the file.txt"""; - [Fact] - public void Absolute_Windows_style_paths_are_lexed_correctly() - { - const string commandText = - @"rm ""c:\temp\the file.txt\"""; + CliCommand command = new ("rm") + { + new CliArgument("arg") + }; - CliCommand command = new("rm") - { - new CliArgument("arg") - }; + var result = CliParser.Parse(command, commandText); - ParseResult result = CliParser.Parse(command, commandText); + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .OnlyContain(a => a == @"/temp/the file.txt"); + } - result.CommandResult - .Tokens - .Should() - .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); - } + [Fact] + public void Absolute_Windows_style_paths_are_lexed_correctly() + { + const string commandText = + @"rm ""c:\temp\the file.txt\"""; + + CliCommand command = new("rm") + { + new CliArgument("arg") + }; + + ParseResult result = CliParser.Parse(command, commandText); + + result.CommandResult + .Tokens + .Should() + .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); + } // Default values /* - [Fact] - public void Commands_can_have_default_argument_values() - { - var argument = new CliArgument("the-arg") - { - DefaultValueFactory = (_) => "default" - }; - - var command = new CliCommand("command") - { - argument - }; + [Fact] + public void Commands_can_have_default_argument_values() + { + var argument = new CliArgument("the-arg") + { + DefaultValueFactory = (_) => "default" + }; - ParseResult result = CliParser.Parse(command, "command"); + var command = new CliCommand("command") + { + argument + }; - GetValue(result, argument) - .Should() - .Be("default"); - } + ParseResult result = CliParser.Parse(command, "command"); - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_the_option_can_still_be_accessed_as_though_it_had_been_applied() - { - var command = new CliCommand("command"); - var option = new CliOption("-o", "--option") - { - DefaultValueFactory = (_) => "the-default" - }; - command.Options.Add(option); + GetValue(result, argument) + .Should() + .Be("default"); + } - ParseResult result = CliParser.Parse(command, "command"); + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_the_option_can_still_be_accessed_as_though_it_had_been_applied() + { + var command = new CliCommand("command"); + var option = new CliOption("-o", "--option") + { + DefaultValueFactory = (_) => "the-default" + }; + command.Options.Add(option); - result.GetResult(option).Should().NotBeNull(); - GetValue(result, option).Should().Be("the-default"); - } + ParseResult result = CliParser.Parse(command, "command"); - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_the_option_result_is_implicit() - { - var option = new CliOption("-o", "--option") - { - DefaultValueFactory = (_) => "the-default" - }; + result.GetResult(option).Should().NotBeNull(); + GetValue(result, option).Should().Be("the-default"); + } - var command = new CliCommand("command") - { - option - }; + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_the_option_result_is_implicit() + { + var option = new CliOption("-o", "--option") + { + DefaultValueFactory = (_) => "the-default" + }; - var result = CliParser.Parse(command, "command"); + var command = new CliCommand("command") + { + option + }; - result.GetResult(option) - .Implicit - .Should() - .BeTrue(); - } + var result = CliParser.Parse(command, "command"); - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_there_are_no_tokens() - { - var option = new CliOption("-o") - { - DefaultValueFactory = (_) => "the-default" - }; + result.GetResult(option) + .Implicit + .Should() + .BeTrue(); + } - var command = new CliCommand("command") - { - option - }; + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_there_are_no_tokens() + { + var option = new CliOption("-o") + { + DefaultValueFactory = (_) => "the-default" + }; - var result = CliParser.Parse(command, "command"); + var command = new CliCommand("command") + { + option + }; - result.GetResult(option) - .IdentifierToken - .Should() - .BeEquivalentTo(default(CliToken)); - } + var result = CliParser.Parse(command, "command"); - [Fact] - public void When_an_argument_with_a_default_value_is_not_matched_then_there_are_no_tokens() - { - var argument = new CliArgument("o") - { - DefaultValueFactory = (_) => "the-default" - }; + result.GetResult(option) + .IdentifierToken + .Should() + .BeEquivalentTo(default(CliToken)); + } - var command = new CliCommand("command") - { - argument - }; - var result = CliParser.Parse(command, "command"); - - result.GetResult(argument) - .Tokens - .Should() - .BeEmpty(); - } + [Fact] + public void When_an_argument_with_a_default_value_is_not_matched_then_there_are_no_tokens() + { + var argument = new CliArgument("o") + { + DefaultValueFactory = (_) => "the-default" + }; + + var command = new CliCommand("command") + { + argument + }; + var result = CliParser.Parse(command, "command"); + + result.GetResult(argument) + .Tokens + .Should() + .BeEmpty(); + } - [Fact] - public void Command_default_argument_value_does_not_override_parsed_value() - { - var argument = new CliArgument("the-arg") - { - DefaultValueFactory = (_) => new DirectoryInfo(Directory.GetCurrentDirectory()) - }; + [Fact] + public void Command_default_argument_value_does_not_override_parsed_value() + { + var argument = new CliArgument("the-arg") + { + DefaultValueFactory = (_) => new DirectoryInfo(Directory.GetCurrentDirectory()) + }; - var command = new CliCommand("inner") - { - argument - }; + var command = new CliCommand("inner") + { + argument + }; - var result = CliParser.Parse(command, "the-directory"); + var result = CliParser.Parse(command, "the-directory"); - GetValue(result, argument) - .Name - .Should() - .Be("the-directory"); - } + GetValue(result, argument) + .Name + .Should() + .Be("the-directory"); + } */ - [Fact] - public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_tokens() + [Fact] + public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_tokens() + { + var outer = new CliCommand("outer") + { + new CliCommand("inner") { - var outer = new CliCommand("outer") + new CliArgument("arg") { - new CliCommand("inner") - { - new CliArgument("arg") - { - Arity = ArgumentArity.OneOrMore - } - } - }; - - ParseResult result = CliParser.Parse(outer, "outer inner -p:RandomThing=random"); - - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("-p:RandomThing=random"); + Arity = ArgumentArity.OneOrMore + } } + }; + + ParseResult result = CliParser.Parse(outer, "outer inner -p:RandomThing=random"); + + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("-p:RandomThing=random"); + } /* - [Fact] - public void The_default_behavior_of_unmatched_tokens_resulting_in_errors_can_be_turned_off() - { + [Fact] + public void The_default_behavior_of_unmatched_tokens_resulting_in_errors_can_be_turned_off() + { // TODO: uses UnmatchedTokens, TreatUnmatchedTokensAsErrors, fix - var command = new CliCommand("the-command") - { - new CliArgument("arg") - }; - command.TreatUnmatchedTokensAsErrors = false; + var command = new CliCommand("the-command") + { + new CliArgument("arg") + }; + command.TreatUnmatchedTokensAsErrors = false; - ParseResult result = command.Parse("the-command arg1 arg2"); + ParseResult result = command.Parse("the-command arg1 arg2"); - result.Errors.Should().BeEmpty(); + result.Errors.Should().BeEmpty(); - result.UnmatchedTokens - .Should() - .BeEquivalentTo("arg2"); - } + result.UnmatchedTokens + .Should() + .BeEquivalentTo("arg2"); + } */ - [Fact] - public void Option_and_Command_can_have_the_same_alias() - { - var innerCommand = new CliCommand("inner") - { - new CliArgument("arg1") - }; + [Fact] + public void Option_and_Command_can_have_the_same_alias() + { + var innerCommand = new CliCommand("inner") + { + new CliArgument("arg1") + }; - var option = new CliOption("--inner"); + var option = new CliOption("--inner"); - var outerCommand = new CliCommand("outer") - { - innerCommand, - option, - new CliArgument("arg2") - }; + var outerCommand = new CliCommand("outer") + { + innerCommand, + option, + new CliArgument("arg2") + }; CliParser.Parse(outerCommand, "outer inner") - .CommandResult - .Command - .Should() - .BeSameAs(innerCommand); + .CommandResult + .Command + .Should() + .BeSameAs(innerCommand); CliParser.Parse(outerCommand, "outer --inner") - .CommandResult - .Command - .Should() - .BeSameAs(outerCommand); + .CommandResult + .Command + .Should() + .BeSameAs(outerCommand); CliParser.Parse(outerCommand, "outer --inner inner") - .CommandResult - .Command - .Should() - .BeSameAs(innerCommand); + .CommandResult + .Command + .Should() + .BeSameAs(innerCommand); CliParser.Parse(outerCommand, "outer --inner inner") - .CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .Contain(o => ((OptionResult)o).Option == option); - } + .CommandResult + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .Contain(o => ((OptionResult)o).Option == option); + } - [Fact] - public void Options_can_have_the_same_alias_differentiated_only_by_prefix() - { - var option1 = new CliOption("-a"); - var option2 = new CliOption("--a"); + [Fact] + public void Options_can_have_the_same_alias_differentiated_only_by_prefix() + { + var option1 = new CliOption("-a"); + var option2 = new CliOption("--a"); - var rootCommand = new CliRootCommand - { - option1, - option2 - }; + var rootCommand = new CliRootCommand + { + option1, + option2 + }; CliParser.Parse(rootCommand, "-a").CommandResult - .Children - .Select(s => ((OptionResult)s).Option) - .Should() - .BeEquivalentTo(option1); + .Children + .Select(s => ((OptionResult)s).Option) + .Should() + .BeEquivalentTo(option1); CliParser.Parse(rootCommand, "--a").CommandResult - .Children - .Select(s => ((OptionResult)s).Option) - .Should() - .BeEquivalentTo(option2); - } + .Children + .Select(s => ((OptionResult)s).Option) + .Should() + .BeEquivalentTo(option2); + } - [Theory] - [InlineData("-x", "\"hello\"")] - [InlineData("-x=", "\"hello\"")] - [InlineData("-x:", "\"hello\"")] - [InlineData("-x", "\"\"")] - [InlineData("-x=", "\"\"")] - [InlineData("-x:", "\"\"")] - public void When_an_option_argument_is_enclosed_in_double_quotes_its_value_retains_the_quotes( - string arg1, - string arg2) - { - var option = new CliOption("-x"); + [Theory] + [InlineData("-x", "\"hello\"")] + [InlineData("-x=", "\"hello\"")] + [InlineData("-x:", "\"hello\"")] + [InlineData("-x", "\"\"")] + [InlineData("-x=", "\"\"")] + [InlineData("-x:", "\"\"")] + public void When_an_option_argument_is_enclosed_in_double_quotes_its_value_retains_the_quotes( + string arg1, + string arg2) + { + var option = new CliOption("-x"); var rootCommand = new CliRootCommand { option }; - var parseResult = CliParser.Parse(rootCommand, new[] { arg1, arg2 }); - - parseResult - .GetResult(option) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo(new[] { arg2 }); - } - - [Fact] // https://github.com/dotnet/command-line-api/issues/1445 - public void Trailing_option_delimiters_are_ignored() - { - var rootCommand = new CliRootCommand - { - new CliCommand("subcommand") - { - new CliOption("--directory") - } - }; - - var args = new[] { "subcommand", "--directory:", @"c:\" }; - - var result = CliParser.Parse(rootCommand, args); + var parseResult = CliParser.Parse(rootCommand, new[] { arg1, arg2 }); - result.Errors.Should().BeEmpty(); + parseResult + .GetResult(option) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo(new[] { arg2 }); + } - result.Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentSequenceTo(new[] { "subcommand", "--directory", @"c:\" }); + [Fact] // https://github.com/dotnet/command-line-api/issues/1445 + public void Trailing_option_delimiters_are_ignored() + { + var rootCommand = new CliRootCommand + { + new CliCommand("subcommand") + { + new CliOption("--directory") } + }; - [Theory] - [InlineData("-x -y")] - [InlineData("-x=-y")] - [InlineData("-x:-y")] - public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_options(string input) - { - var optionX = new CliOption("-x"); + var args = new[] { "subcommand", "--directory:", @"c:\" }; - var command = new CliCommand("command") - { - optionX, - new CliOption("-z") - }; + var result = CliParser.Parse(rootCommand, args); - var result = CliParser.Parse(command, input); + result.Errors.Should().BeEmpty(); - GetValue(result, optionX).Should().Be("-y"); - } + result.Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentSequenceTo(new[] { "subcommand", "--directory", @"c:\" }); + } - [Fact] - public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_bundled_options() - { - var optionA = new CliOption("-a"); - var optionB = new CliOption("-b"); - var optionC = new CliOption("-c"); + [Theory] + [InlineData("-x -y")] + [InlineData("-x=-y")] + [InlineData("-x:-y")] + public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_options(string input) + { + var optionX = new CliOption("-x"); - var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + var command = new CliCommand("command") + { + optionX, + new CliOption("-z") + }; - var result = CliParser.Parse(command, "-a -bc"); + var result = CliParser.Parse(command, input); - GetValue(result, optionA).Should().Be("-bc"); - GetValue(result, optionB).Should().BeFalse(); - GetValue(result, optionC).Should().BeFalse(); - } + GetValue(result, optionX).Should().Be("-y"); + } - [Fact] - public void Option_arguments_can_match_subcommands() - { - var optionA = new CliOption("-a"); - var rootCommand = new CliRootCommand - { - new CliCommand("subcommand"), - optionA - }; + [Fact] + public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_bundled_options() + { + var optionA = new CliOption("-a"); + var optionB = new CliOption("-b"); + var optionC = new CliOption("-c"); + + var command = new CliRootCommand + { + optionA, + optionB, + optionC + }; + + var result = CliParser.Parse(command, "-a -bc"); + + GetValue(result, optionA).Should().Be("-bc"); + GetValue(result, optionB).Should().BeFalse(); + GetValue(result, optionC).Should().BeFalse(); + } - var result = CliParser.Parse(rootCommand, "-a subcommand"); + [Fact] + public void Option_arguments_can_match_subcommands() + { + var optionA = new CliOption("-a"); + var rootCommand = new CliRootCommand + { + new CliCommand("subcommand"), + optionA + }; - GetValue(result, optionA).Should().Be("subcommand"); - result.CommandResult.Command.Should().BeSameAs(rootCommand); - } + var result = CliParser.Parse(rootCommand, "-a subcommand"); - [Fact] - public void Arguments_can_match_subcommands() - { - var argument = new CliArgument("arg"); - var subcommand = new CliCommand("subcommand") - { - argument - }; - var rootCommand = new CliRootCommand - { - subcommand - }; + GetValue(result, optionA).Should().Be("subcommand"); + result.CommandResult.Command.Should().BeSameAs(rootCommand); + } - var result = CliParser.Parse(rootCommand, "subcommand one two three subcommand four"); + [Fact] + public void Arguments_can_match_subcommands() + { + var argument = new CliArgument("arg"); + var subcommand = new CliCommand("subcommand") + { + argument + }; + var rootCommand = new CliRootCommand + { + subcommand + }; - result.CommandResult.Command.Should().BeSameAs(subcommand); + var result = CliParser.Parse(rootCommand, "subcommand one two three subcommand four"); - GetValue(result, argument) - .Should() - .BeEquivalentSequenceTo("one", "two", "three", "subcommand", "four"); - } + result.CommandResult.Command.Should().BeSameAs(subcommand); - [Theory] - [InlineData("-x=-y")] - [InlineData("-x:-y")] - public void Option_arguments_can_match_the_aliases_of_sibling_options_when_non_space_argument_delimiter_is_used(string input) - { - var optionX = new CliOption("-x"); + GetValue(result, argument) + .Should() + .BeEquivalentSequenceTo("one", "two", "three", "subcommand", "four"); + } - var command = new CliCommand("command") - { - optionX, - new CliOption("-y") - }; + [Theory] + [InlineData("-x=-y")] + [InlineData("-x:-y")] + public void Option_arguments_can_match_the_aliases_of_sibling_options_when_non_space_argument_delimiter_is_used(string input) + { + var optionX = new CliOption("-x"); - var result = CliParser.Parse(command, input); + var command = new CliCommand("command") + { + optionX, + new CliOption("-y") + }; - result.Errors.Should().BeEmpty(); - GetValue(result, optionX).Should().Be("-y"); - } + var result = CliParser.Parse(command, input); - [Fact] - public void Single_option_arguments_that_match_option_aliases_are_parsed_correctly() - { - var optionX = new CliOption("-x"); + result.Errors.Should().BeEmpty(); + GetValue(result, optionX).Should().Be("-y"); + } - var command = new CliRootCommand - { - optionX - }; + [Fact] + public void Single_option_arguments_that_match_option_aliases_are_parsed_correctly() + { + var optionX = new CliOption("-x"); - var result = CliParser.Parse(command, "-x -x"); + var command = new CliRootCommand + { + optionX + }; - GetValue(result, optionX).Should().Be("-x"); - } + var result = CliParser.Parse(command, "-x -x"); - [Theory] - [InlineData("-x -y")] - [InlineData("-x true -y")] - [InlineData("-x:true -y")] - [InlineData("-x=true -y")] - [InlineData("-x -y true")] - [InlineData("-x true -y true")] - [InlineData("-x:true -y:true")] - [InlineData("-x=true -y:true")] - public void Boolean_options_are_not_greedy(string commandLine) - { - var optX = new CliOption("-x"); - var optY = new CliOption("-y"); + GetValue(result, optionX).Should().Be("-x"); + } - var root = new CliRootCommand() - { - optX, - optY, - }; + [Theory] + [InlineData("-x -y")] + [InlineData("-x true -y")] + [InlineData("-x:true -y")] + [InlineData("-x=true -y")] + [InlineData("-x -y true")] + [InlineData("-x true -y true")] + [InlineData("-x:true -y:true")] + [InlineData("-x=true -y:true")] + public void Boolean_options_are_not_greedy(string commandLine) + { + var optX = new CliOption("-x"); + var optY = new CliOption("-y"); - var result = CliParser.Parse(root, commandLine); + var root = new CliRootCommand() + { + optX, + optY, + }; - result.Errors.Should().BeEmpty(); + var result = CliParser.Parse(root, commandLine); - GetValue(result, optX).Should().BeTrue(); - GetValue(result, optY).Should().BeTrue(); - } + result.Errors.Should().BeEmpty(); - [Fact] - public void Multiple_option_arguments_that_match_multiple_arity_option_aliases_are_parsed_correctly() - { - var optionX = new CliOption("-x"); - var optionY = new CliOption("-y"); + GetValue(result, optX).Should().BeTrue(); + GetValue(result, optY).Should().BeTrue(); + } - var command = new CliRootCommand - { - optionX, - optionY - }; + [Fact] + public void Multiple_option_arguments_that_match_multiple_arity_option_aliases_are_parsed_correctly() + { + var optionX = new CliOption("-x"); + var optionY = new CliOption("-y"); - var result = CliParser.Parse(command, "-x -x -x -y -y -x -y -y -y -x -x -y"); + var command = new CliRootCommand + { + optionX, + optionY + }; - GetValue(result, optionX).Should().BeEquivalentTo(new[] { "-x", "-y", "-y" }); - GetValue(result, optionY).Should().BeEquivalentTo(new[] { "-x", "-y", "-x" }); - } + var result = CliParser.Parse(command, "-x -x -x -y -y -x -y -y -y -x -x -y"); - [Fact] - public void Bundled_option_arguments_that_match_option_aliases_are_parsed_correctly() - { - var optionX = new CliOption("-x"); - var optionY = new CliOption("-y"); + GetValue(result, optionX).Should().BeEquivalentTo(new[] { "-x", "-y", "-y" }); + GetValue(result, optionY).Should().BeEquivalentTo(new[] { "-x", "-y", "-x" }); + } - var command = new CliRootCommand - { - optionX, - optionY - }; + [Fact] + public void Bundled_option_arguments_that_match_option_aliases_are_parsed_correctly() + { + var optionX = new CliOption("-x"); + var optionY = new CliOption("-y"); - var result = CliParser.Parse(command, "-yxx"); + var command = new CliRootCommand + { + optionX, + optionY + }; - GetValue(result, optionX).Should().Be("x"); - } + var result = CliParser.Parse(command, "-yxx"); - [Fact] - public void Argument_name_is_not_matched_as_a_token() - { - var nameArg = new CliArgument("name"); - var columnsArg = new CliArgument>("columns"); + GetValue(result, optionX).Should().Be("x"); + } - var command = new CliCommand("add") - { - nameArg, - columnsArg - }; + [Fact] + public void Argument_name_is_not_matched_as_a_token() + { + var nameArg = new CliArgument("name"); + var columnsArg = new CliArgument>("columns"); - var result = CliParser.Parse(command, "name one two three"); + var command = new CliCommand("add") + { + nameArg, + columnsArg + }; - GetValue(result, nameArg).Should().Be("name"); - GetValue(result, columnsArg).Should().BeEquivalentTo("one", "two", "three"); - } + var result = CliParser.Parse(command, "name one two three"); - [Fact] - public void Option_aliases_do_not_need_to_be_prefixed() - { - var option = new CliOption("noprefix"); + GetValue(result, nameArg).Should().Be("name"); + GetValue(result, columnsArg).Should().BeEquivalentTo("one", "two", "three"); + } + + [Fact] + public void Option_aliases_do_not_need_to_be_prefixed() + { + var option = new CliOption("noprefix"); var rootCommand = new CliRootCommand { option }; var result = CliParser.Parse(rootCommand, "noprefix"); - result.GetResult(option).Should().NotBeNull(); - } + result.GetResult(option).Should().NotBeNull(); + } - [Fact] - public void Boolean_options_with_no_argument_specified_do_not_match_subsequent_arguments() - { - var option = new CliOption("-v"); + [Fact] + public void Boolean_options_with_no_argument_specified_do_not_match_subsequent_arguments() + { + var option = new CliOption("-v"); - var command = new CliCommand("command") - { - option - }; + var command = new CliCommand("command") + { + option + }; - var result = CliParser.Parse(command, "-v an-argument"); + var result = CliParser.Parse(command, "-v an-argument"); - GetValue(result, option).Should().BeTrue(); - } + GetValue(result, option).Should().BeTrue(); + } /* - [Fact] - public void When_a_command_line_has_unmatched_tokens_they_are_not_applied_to_subsequent_options() - { - // TODO: uses TreatUnmatchedTokensAsErrors, fix - var command = new CliCommand("command") - { - TreatUnmatchedTokensAsErrors = false - }; - var optionX = new CliOption("-x"); - command.Options.Add(optionX); - var optionY = new CliOption("-y"); - command.Options.Add(optionY); - - var result = command.Parse("-x 23 unmatched-token -y 42"); - - GetValue(result, optionX).Should().Be("23"); - GetValue(result, optionY).Should().Be("42"); - result.UnmatchedTokens.Should().BeEquivalentTo("unmatched-token"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void When_a_command_line_has_unmatched_tokens_the_parse_result_action_should_depend_on_parsed_command_TreatUnmatchedTokensAsErrors(bool treatUnmatchedTokensAsErrors) - { - // TODO: uses TreatUnmatchedTokensAsErrors, fix - CliRootCommand rootCommand = new(); - CliCommand subcommand = new("vstest") - { - new CliOption("--Platform"), - new CliOption("--Framework"), - new CliOption("--logger") - }; - subcommand.TreatUnmatchedTokensAsErrors = treatUnmatchedTokensAsErrors; - rootCommand.Subcommands.Add(subcommand); - - var result = rootCommand.Parse("vstest test1.dll test2.dll"); - - result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); - - if (treatUnmatchedTokensAsErrors) - { - result.Errors.Should().NotBeEmpty(); - result.Action.Should().NotBeSameAs(result.CommandResult.Command.Action); - } - else - { - result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); - } - } - - [Fact] - public void RootCommand_TreatUnmatchedTokensAsErrors_set_to_false_has_precedence_over_subcommands() - { + [Fact] + public void When_a_command_line_has_unmatched_tokens_they_are_not_applied_to_subsequent_options() + { // TODO: uses TreatUnmatchedTokensAsErrors, fix - CliRootCommand rootCommand = new(); - rootCommand.TreatUnmatchedTokensAsErrors = false; - CliCommand subcommand = new("vstest") - { - new CliOption("--Platform"), - new CliOption("--Framework"), - new CliOption("--logger") - }; - subcommand.TreatUnmatchedTokensAsErrors = true; // the default, set to true to make it explicit - rootCommand.Subcommands.Add(subcommand); + var command = new CliCommand("command") + { + TreatUnmatchedTokensAsErrors = false + }; + var optionX = new CliOption("-x"); + command.Options.Add(optionX); + var optionY = new CliOption("-y"); + command.Options.Add(optionY); + + var result = command.Parse("-x 23 unmatched-token -y 42"); + + GetValue(result, optionX).Should().Be("23"); + GetValue(result, optionY).Should().Be("42"); + result.UnmatchedTokens.Should().BeEquivalentTo("unmatched-token"); + } - var result = rootCommand.Parse("vstest test1.dll test2.dll"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void When_a_command_line_has_unmatched_tokens_the_parse_result_action_should_depend_on_parsed_command_TreatUnmatchedTokensAsErrors(bool treatUnmatchedTokensAsErrors) + { + // TODO: uses TreatUnmatchedTokensAsErrors, fix + CliRootCommand rootCommand = new(); + CliCommand subcommand = new("vstest") + { + new CliOption("--Platform"), + new CliOption("--Framework"), + new CliOption("--logger") + }; + subcommand.TreatUnmatchedTokensAsErrors = treatUnmatchedTokensAsErrors; + rootCommand.Subcommands.Add(subcommand); + + var result = rootCommand.Parse("vstest test1.dll test2.dll"); + + result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); + + if (treatUnmatchedTokensAsErrors) + { + result.Errors.Should().NotBeEmpty(); + result.Action.Should().NotBeSameAs(result.CommandResult.Command.Action); + } + else + { + result.Errors.Should().BeEmpty(); + result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + } + } - result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); + [Fact] + public void RootCommand_TreatUnmatchedTokensAsErrors_set_to_false_has_precedence_over_subcommands() + { + // TODO: uses TreatUnmatchedTokensAsErrors, fix + CliRootCommand rootCommand = new(); + rootCommand.TreatUnmatchedTokensAsErrors = false; + CliCommand subcommand = new("vstest") + { + new CliOption("--Platform"), + new CliOption("--Framework"), + new CliOption("--logger") + }; + subcommand.TreatUnmatchedTokensAsErrors = true; // the default, set to true to make it explicit + rootCommand.Subcommands.Add(subcommand); + + var result = rootCommand.Parse("vstest test1.dll test2.dll"); + + result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); - result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); - } + result.Errors.Should().BeEmpty(); + result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + } */ [Fact] - public void Parse_can_not_be_called_with_null_args() - { - Action passNull = () => CliParser.Parse(new CliRootCommand(), args: null); + public void Parse_can_not_be_called_with_null_args() + { + Action passNull = () => CliParser.Parse(new CliRootCommand(), args: null); - passNull.Should().Throw(); - } + passNull.Should().Throw(); + } - [Fact] - public void Command_argument_arity_can_be_a_fixed_value_greater_than_1() - { - var argument = new CliArgument("arg") - { - Arity = new ArgumentArity(3, 3) - }; - var command = new CliCommand("the-command") - { - argument - }; + [Fact] + public void Command_argument_arity_can_be_a_fixed_value_greater_than_1() + { + var argument = new CliArgument("arg") + { + Arity = new ArgumentArity(3, 3) + }; + var command = new CliCommand("the-command") + { + argument + }; CliParser.Parse(command, "1 2 3") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument), - new CliToken("2", CliTokenType.Argument, argument), - new CliToken("3", CliTokenType.Argument, argument)); + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument), + new CliToken("2", CliTokenType.Argument, argument), + new CliToken("3", CliTokenType.Argument, argument)); } - [Fact] - public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_than_1() - { - var argument = new CliArgument("arg") - { - Arity = new ArgumentArity(3, 5) - }; - var command = new CliCommand("the-command") - { - argument - }; + [Fact] + public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_than_1() + { + var argument = new CliArgument("arg") + { + Arity = new ArgumentArity(3, 5) + }; + var command = new CliCommand("the-command") + { + argument + }; CliParser.Parse(command, "1 2 3") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument), - new CliToken("2", CliTokenType.Argument, argument), - new CliToken("3", CliTokenType.Argument, argument)); + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument), + new CliToken("2", CliTokenType.Argument, argument), + new CliToken("3", CliTokenType.Argument, argument)); CliParser.Parse(command, "1 2 3 4 5") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument), - new CliToken("2", CliTokenType.Argument, argument), - new CliToken("3", CliTokenType.Argument, argument), - new CliToken("4", CliTokenType.Argument, argument), - new CliToken("5", CliTokenType.Argument, argument)); - } + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument), + new CliToken("2", CliTokenType.Argument, argument), + new CliToken("3", CliTokenType.Argument, argument), + new CliToken("4", CliTokenType.Argument, argument), + new CliToken("5", CliTokenType.Argument, argument)); + } // TODO: Validation? /* - [Fact] - public void When_command_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() + [Fact] + public void When_command_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() + { + var command = new CliCommand("the-command") + { + new CliArgument("arg") { - var command = new CliCommand("the-command") - { - new CliArgument("arg") - { - Arity = new ArgumentArity(2, 3) - } - }; - - var result = CliParser.Parse(command, "1"); - - result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(command.Arguments[0]))); + Arity = new ArgumentArity(2, 3) } + }; - [Fact] - public void When_command_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() + var result = CliParser.Parse(command, "1"); + + result.Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(command.Arguments[0]))); + } + + [Fact] + public void When_command_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() + { + var command = new CliCommand("the-command") + { + new CliArgument("arg") { - var command = new CliCommand("the-command") - { - new CliArgument("arg") - { - Arity = new ArgumentArity(2, 3) - } - }; - - ParseResult parseResult = CliParser.Parse(command, "1 2 3 4"); - - parseResult - .Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); + Arity = new ArgumentArity(2, 3) } + }; + + ParseResult parseResult = CliParser.Parse(command, "1 2 3 4"); + + parseResult + .Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); + } */ - [Fact] - public void Option_argument_arity_can_be_a_fixed_value_greater_than_1() - { - var option = new CliOption("-x") { Arity = new ArgumentArity(3, 3)}; + [Fact] + public void Option_argument_arity_can_be_a_fixed_value_greater_than_1() + { + var option = new CliOption("-x") { Arity = new ArgumentArity(3, 3)}; - var command = new CliCommand("the-command") - { - option - }; + var command = new CliCommand("the-command") + { + option + }; CliParser.Parse(command, "-x 1 -x 2 -x 3") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default), - new CliToken("2", CliTokenType.Argument, default), - new CliToken("3", CliTokenType.Argument, default)); - } + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default), + new CliToken("2", CliTokenType.Argument, default), + new CliToken("3", CliTokenType.Argument, default)); + } - [Fact] - public void Option_argument_arity_can_be_a_range_with_a_lower_bound_greater_than_1() - { - var option = new CliOption("-x") { Arity = new ArgumentArity(3, 5) }; + [Fact] + public void Option_argument_arity_can_be_a_range_with_a_lower_bound_greater_than_1() + { + var option = new CliOption("-x") { Arity = new ArgumentArity(3, 5) }; - var command = new CliCommand("the-command") - { - option - }; + var command = new CliCommand("the-command") + { + option + }; CliParser.Parse(command, "-x 1 -x 2 -x 3") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default), - new CliToken("2", CliTokenType.Argument, default), - new CliToken("3", CliTokenType.Argument, default)); + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default), + new CliToken("2", CliTokenType.Argument, default), + new CliToken("3", CliTokenType.Argument, default)); CliParser.Parse(command, "-x 1 -x 2 -x 3 -x 4 -x 5") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default), - new CliToken("2", CliTokenType.Argument, default), - new CliToken("3", CliTokenType.Argument, default), - new CliToken("4", CliTokenType.Argument, default), - new CliToken("5", CliTokenType.Argument, default)); - } + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default), + new CliToken("2", CliTokenType.Argument, default), + new CliToken("3", CliTokenType.Argument, default), + new CliToken("4", CliTokenType.Argument, default), + new CliToken("5", CliTokenType.Argument, default)); + } // TODO: Validation? -/* - [Fact] - public void When_option_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() - { - var option = new CliOption("-x") - { - Arity = new ArgumentArity(2, 3) - }; +/* + [Fact] + public void When_option_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() + { + var option = new CliOption("-x") + { + Arity = new ArgumentArity(2, 3) + }; - var command = new CliCommand("the-command") - { - option - }; + var command = new CliCommand("the-command") + { + option + }; - var result = CliParser.Parse(command, "-x 1"); + var result = CliParser.Parse(command, "-x 1"); - result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(option))); - } + result.Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(option))); + } - [Fact] - public void When_option_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() - { - var command = new CliCommand("the-command") - { - new CliOption("-x") { Arity = new ArgumentArity(2, 3)} - }; + [Fact] + public void When_option_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() + { + var command = new CliCommand("the-command") + { + new CliOption("-x") { Arity = new ArgumentArity(2, 3)} + }; CliParser.Parse(command, "-x 1 2 3 4") - .Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); - } + .Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); + } */ - [Fact] - public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_option() - { - var rootCommand = new CliCommand("jdbc"); - rootCommand.Add(new CliOption("url")); - var result = CliParser.Parse(rootCommand, "jdbc url \"jdbc:sqlserver://10.0.0.2;databaseName=main\""); - - result.Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("url", - "jdbc:sqlserver://10.0.0.2;databaseName=main"); - } + [Fact] + public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_option() + { + var rootCommand = new CliCommand("jdbc"); + rootCommand.Add(new CliOption("url")); + var result = CliParser.Parse(rootCommand, "jdbc url \"jdbc:sqlserver://10.0.0.2;databaseName=main\""); + + result.Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("url", + "jdbc:sqlserver://10.0.0.2;databaseName=main"); + } /* - [Fact] - public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() - { - // TODO: uses GetCompletions, fix - // Tests bug identified in https://github.com/dotnet/command-line-api/issues/997 + [Fact] + public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() + { + // TODO: uses GetCompletions, fix + // Tests bug identified in https://github.com/dotnet/command-line-api/issues/997 - var argument1 = new CliArgument("arg1"); + var argument1 = new CliArgument("arg1"); - var argument2 = new CliArgument("arg2"); + var argument2 = new CliArgument("arg2"); - var command = new CliCommand("subcommand") - { - argument1, - argument2 - }; + var command = new CliCommand("subcommand") + { + argument1, + argument2 + }; - var rootCommand = new CliRootCommand - { - command - }; + var rootCommand = new CliRootCommand + { + command + }; - var parseResult = rootCommand.Parse("subcommand arg1 arg2"); + var parseResult = rootCommand.Parse("subcommand arg1 arg2"); - Action act = () => parseResult.GetCompletions(); - act.Should().NotThrow(); - } + Action act = () => parseResult.GetCompletions(); + act.Should().NotThrow(); + } */ - [Theory] // https://github.com/dotnet/command-line-api/issues/1551, https://github.com/dotnet/command-line-api/issues/1533 - [InlineData("--exec-prefix", "")] - [InlineData("--exec-prefix:", "")] - [InlineData("--exec-prefix=", "")] - public void Parsed_value_of_empty_string_arg_is_an_empty_string(string arg1, string arg2) - { - var option = new CliOption("--exec-prefix") - { - DefaultValueFactory = _ => "/usr/local" - }; + [Theory] // https://github.com/dotnet/command-line-api/issues/1551, https://github.com/dotnet/command-line-api/issues/1533 + [InlineData("--exec-prefix", "")] + [InlineData("--exec-prefix:", "")] + [InlineData("--exec-prefix=", "")] + public void Parsed_value_of_empty_string_arg_is_an_empty_string(string arg1, string arg2) + { + var option = new CliOption("--exec-prefix") + { + DefaultValueFactory = _ => "/usr/local" + }; - var rootCommand = new CliRootCommand - { - option - }; + var rootCommand = new CliRootCommand + { + option + }; - var result = CliParser.Parse(rootCommand, new[] { arg1, arg2 }); + var result = CliParser.Parse(rootCommand, new[] { arg1, arg2 }); - GetValue(result, option).Should().BeEmpty(); - } - /* - */ + GetValue(result, option).Should().BeEmpty(); + } } } From 0f02d19ccb34be7a73271b2c92d99702991e07b3 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 2 Apr 2024 16:08:24 -0400 Subject: [PATCH 038/150] Add previous commit to .git-blame-ignore-revs --- .git-blame-ignore-revs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..3e2257721b --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,8 @@ +# .git-blame-ignore-revs +# To configure git to use this file, run +# git config blame.ignoreRevsFile .git-blame-ignore-revs +# +# Testing git-blame-ingore-revs on small scale +# before considering broader use, by reverting +# bad autoformatter changes +1aca22a0350e800b72f2275df12a58f0a1f72101 From 032cb10c077234c195c6d653214eaae400cc7708 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Wed, 3 Apr 2024 20:37:30 -0400 Subject: [PATCH 039/150] Tokenizer now tracks the location of tokens * Preprocessed tokens can be skipped without messing up the location of subsequent tokens. * Tokens from response files can have accurate location information. This will enable better error handling and diagramming. --- src/System.CommandLine.Tests/ParserTests.cs | 55 +- .../TokenizerTests.cs | 59 ++- src/System.CommandLine/CliConfiguration.cs | 44 +- src/System.CommandLine/ParseResult.cs | 1 + src/System.CommandLine/Parsing/CliParser.cs | 5 +- src/System.CommandLine/Parsing/CliToken.cs | 20 +- src/System.CommandLine/Parsing/Location.cs | 56 ++ .../Parsing/StringExtensions.cs | 493 ++++++++---------- .../System.CommandLine.csproj | 1 + 9 files changed, 420 insertions(+), 314 deletions(-) create mode 100644 src/System.CommandLine/Parsing/Location.cs diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 8d37106b40..3da60b50a6 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -16,12 +16,21 @@ namespace System.CommandLine.Tests { public partial class ParserTests { + // TODO: Update testing strategy if we use Location in equality. Some will break + private readonly Location dummyLocation = new("", Location.Internal, -1, null); + private T GetValue(ParseResult parseResult, CliOption option) => parseResult.GetValue(option); private T GetValue(ParseResult parseResult, CliArgument argument) => parseResult.GetValue(argument); + //[Fact] + //public void FailureTest() + //{ + // Assert.True(false); + //} + [Fact] public void An_option_can_be_checked_by_object_instance() { @@ -1447,10 +1456,10 @@ public void Command_argument_arity_can_be_a_fixed_value_greater_than_1() .Tokens .Should() .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument), - new CliToken("2", CliTokenType.Argument, argument), - new CliToken("3", CliTokenType.Argument, argument)); - } + new CliToken("1", CliTokenType.Argument, argument,dummyLocation), + new CliToken("2", CliTokenType.Argument, argument, dummyLocation), + new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); + } [Fact] public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_than_1() @@ -1469,19 +1478,19 @@ public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_tha .Tokens .Should() .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument), - new CliToken("2", CliTokenType.Argument, argument), - new CliToken("3", CliTokenType.Argument, argument)); + new CliToken("1", CliTokenType.Argument, argument, dummyLocation), + new CliToken("2", CliTokenType.Argument, argument, dummyLocation), + new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); CliParser.Parse(command, "1 2 3 4 5") .CommandResult .Tokens .Should() .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument), - new CliToken("2", CliTokenType.Argument, argument), - new CliToken("3", CliTokenType.Argument, argument), - new CliToken("4", CliTokenType.Argument, argument), - new CliToken("5", CliTokenType.Argument, argument)); + new CliToken("1", CliTokenType.Argument, argument, dummyLocation), + new CliToken("2", CliTokenType.Argument, argument, dummyLocation), + new CliToken("3", CliTokenType.Argument, argument, dummyLocation), + new CliToken("4", CliTokenType.Argument, argument, dummyLocation), + new CliToken("5", CliTokenType.Argument, argument, dummyLocation)); } // TODO: Validation? @@ -1541,9 +1550,9 @@ public void Option_argument_arity_can_be_a_fixed_value_greater_than_1() .Tokens .Should() .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default), - new CliToken("2", CliTokenType.Argument, default), - new CliToken("3", CliTokenType.Argument, default)); + new CliToken("1", CliTokenType.Argument, default, dummyLocation), + new CliToken("2", CliTokenType.Argument, default, dummyLocation), + new CliToken("3", CliTokenType.Argument, default, dummyLocation)); } [Fact] @@ -1561,19 +1570,19 @@ public void Option_argument_arity_can_be_a_range_with_a_lower_bound_greater_than .Tokens .Should() .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default), - new CliToken("2", CliTokenType.Argument, default), - new CliToken("3", CliTokenType.Argument, default)); + new CliToken("1", CliTokenType.Argument, default, dummyLocation), + new CliToken("2", CliTokenType.Argument, default, dummyLocation), + new CliToken("3", CliTokenType.Argument, default, dummyLocation)); CliParser.Parse(command, "-x 1 -x 2 -x 3 -x 4 -x 5") .GetResult(option) .Tokens .Should() .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default), - new CliToken("2", CliTokenType.Argument, default), - new CliToken("3", CliTokenType.Argument, default), - new CliToken("4", CliTokenType.Argument, default), - new CliToken("5", CliTokenType.Argument, default)); + new CliToken("1", CliTokenType.Argument, default, dummyLocation), + new CliToken("2", CliTokenType.Argument, default, dummyLocation), + new CliToken("3", CliTokenType.Argument, default, dummyLocation), + new CliToken("4", CliTokenType.Argument, default, dummyLocation), + new CliToken("5", CliTokenType.Argument, default, dummyLocation)); } // TODO: Validation? diff --git a/src/System.CommandLine.Tests/TokenizerTests.cs b/src/System.CommandLine.Tests/TokenizerTests.cs index 8f162059a6..1b810e5e43 100644 --- a/src/System.CommandLine.Tests/TokenizerTests.cs +++ b/src/System.CommandLine.Tests/TokenizerTests.cs @@ -15,14 +15,14 @@ public partial class TokenizerTests { [Fact] - public void The_tokenizer_is_accessible() + public void The_tokenizer_can_handle_single_option() { var option = new CliOption("--hello"); var command = new CliRootCommand { option }; IReadOnlyList args = ["--hello", "world"]; List tokens = null; List errors = null; - CliTokenizer.Tokenize(args,command,false, true, out tokens, out errors); + Tokenizer.Tokenize(args, command, new CliConfiguration(command), true, out tokens, out errors); tokens .Skip(1) @@ -32,5 +32,60 @@ public void The_tokenizer_is_accessible() errors.Should().BeNull(); } + + [Fact] + public void Location_stack_is_correct() + { + var option = new CliOption("--hello"); + var command = new CliRootCommand { option }; + IReadOnlyList args = ["--hello", "world"]; + List tokens = null; + List errors = null; + + int rootCommandNameLength = CliExecutable.ExecutableName.Length; + + Tokenizer.Tokenize(args, + command, + new CliConfiguration(command), + true, + out tokens, + out errors); + + var locations = tokens + .Skip(1) + .Select(t => t.Location.ToString()) + .ToList(); + errors.Should().BeNull(); + tokens.Count.Should().Be(3); + locations.Count.Should().Be(2); + locations[0].Should().Be($"User [-1, {rootCommandNameLength}, 0]; User [0, 7, 0]"); + locations[1].Should().Be($"User [-1, {rootCommandNameLength}, 0]; User [1, 5, 0]"); + } + + [Fact] + public void Directives_are_skipped() + { + var option = new CliOption("--hello"); + var command = new CliRootCommand { option }; + var configuration = new CliConfiguration(command); + configuration.AddPreprocessedLocation(new Location("[diagram]", Location.User, 0, null)); + IReadOnlyList args = ["[diagram] --hello", "world"]; + + List tokens = null; + List errors = null; + + Tokenizer.Tokenize(args, + command, + new CliConfiguration(command), + true, + out tokens, + out errors); + + var hasDiagram = tokens + .Any(t => t.Value == "[diagram]"); + errors.Should().BeNull(); + tokens.Count.Should().Be(3); // root is a token + hasDiagram .Should().BeFalse(); + } } } diff --git a/src/System.CommandLine/CliConfiguration.cs b/src/System.CommandLine/CliConfiguration.cs index b5a4e09e26..8b19f68f99 100644 --- a/src/System.CommandLine/CliConfiguration.cs +++ b/src/System.CommandLine/CliConfiguration.cs @@ -55,6 +55,45 @@ public CliConfiguration(CliCommand rootCommand) /// /// public bool EnablePosixBundling { get; set; } = true; + + /// + /// Indicates whether the first argument of the passed string is the exe name + /// + /// The args of a command line, such as those passed to Main(string[] args) + /// + // TODO: If this is the right model, tuck this away because it should only be used by subsystems. + public bool FirstArgumentIsRootCommand(IReadOnlyList args) + { + // TODO: This logic was previously that rawInput was null. Seems more sensible to look for an empty args array.From private static ParseResult Parse(CliCommand ,IReadOnlyList< string > ,string? ,CliConfiguration? ). CHeck logic and ensure test coverage + return args.Any() + ? FirstArgLooksLikeRoot(args.First(), RootCommand) + : false; + + static bool FirstArgLooksLikeRoot(string firstArg, CliCommand rootCommand) + { + try + { + return firstArg == CliExecutable.ExecutablePath || rootCommand.EqualsNameOrAlias(Path.GetFileName(firstArg)); + } + catch // possible exception for illegal characters in path on .NET Framework + { + return false; + } + + } + } + + private List? preprocessedLocations = null; + public IEnumerable? PreProcessedLocations => preprocessedLocations; + public void AddPreprocessedLocation(Location location) + { + if (preprocessedLocations is null) + { + preprocessedLocations = new List(); + } + preprocessedLocations.Add(location); + } + /* /// /// Enables a default exception handler to catch any unhandled exceptions thrown during invocation. Enabled by default. @@ -67,6 +106,7 @@ public CliConfiguration(CliCommand rootCommand) /// If not provided, a default timeout of 2 seconds is enforced. /// public TimeSpan? ProcessTerminationTimeout { get; set; } = TimeSpan.FromSeconds(2); + */ /// /// Response file token replacer, enabled by default. @@ -75,8 +115,8 @@ public CliConfiguration(CliCommand rootCommand) /// /// When enabled, any token prefixed with @ can be replaced with zero or more other tokens. This is mostly commonly used to expand tokens from response files and interpolate them into a command line prior to parsing. /// - public TryReplaceToken? ResponseFileTokenReplacer { get; set; } = StringExtensions.TryReadResponseFile; - */ + public Func? tokens, List? errors)>? ResponseFileTokenReplacer { get; set; } + /// /// Gets the root command. /// diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 35f940e6c1..9c8e96cea6 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -25,6 +25,7 @@ public sealed class ParseResult internal ParseResult( CliConfiguration configuration, +// TODO: determine how rootCommandResult and commandResult differ CommandResult rootCommandResult, CommandResult commandResult, List tokens, diff --git a/src/System.CommandLine/Parsing/CliParser.cs b/src/System.CommandLine/Parsing/CliParser.cs index 0b5fd199c0..1ab9ddd41e 100644 --- a/src/System.CommandLine/Parsing/CliParser.cs +++ b/src/System.CommandLine/Parsing/CliParser.cs @@ -138,6 +138,7 @@ string CurrentToken() bool IsAtEndOfInput() => pos == memory.Length; } + // TODO: I'd like a name change where all refs to the string args passed to main are "args" and arguments refers to CLI arguments private static ParseResult Parse( CliCommand rootCommand, IReadOnlyList arguments, @@ -151,11 +152,11 @@ private static ParseResult Parse( configuration ??= new CliConfiguration(rootCommand); - CliTokenizer.Tokenize( + Tokenizer.Tokenize( arguments, rootCommand, + configuration, inferRootCommand: rawInput is not null, - configuration.EnablePosixBundling, out List tokens, out List? tokenizationErrors); diff --git a/src/System.CommandLine/Parsing/CliToken.cs b/src/System.CommandLine/Parsing/CliToken.cs index 76e7ca2d76..d692285332 100644 --- a/src/System.CommandLine/Parsing/CliToken.cs +++ b/src/System.CommandLine/Parsing/CliToken.cs @@ -3,6 +3,8 @@ namespace System.CommandLine.Parsing { + // TODO: Include location in equality + // FIXME: should CliToken be public or internal? made internal for now // FIXME: should CliToken be a struct? /// @@ -10,35 +12,39 @@ namespace System.CommandLine.Parsing /// internal sealed class CliToken : IEquatable { - internal const int ImplicitPosition = -1; + public static CliToken CreateFromOtherToken(CliToken otherToken, string? arg, Location location) + => new(arg, otherToken.Type, otherToken.Symbol, location); /// The string value of the token. /// The type of the token. /// The symbol represented by the token + /// The location of the token + /* public CliToken(string? value, CliTokenType type, CliSymbol symbol) { Value = value ?? ""; Type = type; Symbol = symbol; - Position = ImplicitPosition; + Location = Location.CreateImplicit(value, value is null ? 0 : value.Length); } - - internal CliToken(string? value, CliTokenType type, CliSymbol? symbol, int position) + */ + + internal CliToken(string? value, CliTokenType type, CliSymbol? symbol, Location location) { Value = value ?? ""; Type = type; Symbol = symbol; - Position = position; + Location = location; } - internal int Position { get; } + internal Location Location { get; } /// /// The string value of the token. /// public string Value { get; } - internal bool Implicit => Position == ImplicitPosition; + internal bool Implicit => Location.IsImplicit; /// /// The type of the token. diff --git a/src/System.CommandLine/Parsing/Location.cs b/src/System.CommandLine/Parsing/Location.cs new file mode 100644 index 0000000000..bfaa877cda --- /dev/null +++ b/src/System.CommandLine/Parsing/Location.cs @@ -0,0 +1,56 @@ +// 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; +using System.Collections.Generic; +using System.ComponentModel; +using static System.Net.Mime.MediaTypeNames; + +namespace System.CommandLine.Parsing +{ + public record Location + { + public const string Implicit = "Implicit"; + public const string Internal = "Internal"; + public const string User = "User"; + public const string Response = "Response"; + + internal static Location CreateRoot(string exeName, bool isImplicit, int start) + => new(exeName, isImplicit ? Internal : User, start, null); + internal static Location CreateImplicit(string text, Location outerLocation, int offset = 0) + => new(text, Implicit, -1, outerLocation, offset); + internal static Location CreateInternal(string text, Location? outerLocation = null, int offset = 0) + => new(text, Internal, -1, outerLocation, offset); + internal static Location CreateUser(string text, int start, Location outerLocation, int offset = 0) + => new(text, User, start, outerLocation, offset); + internal static Location CreateResponse(string responseSourceName, int start, Location outerLocation, int offset = 0) + => new(responseSourceName, $"{Response}:{responseSourceName}", start, outerLocation, offset); + + internal static Location FromOuterLocation(string text, int start, Location outerLocation, int offset = 0) + => new(text, outerLocation.Source, start, outerLocation, offset); + + public Location(string text, string source, int start, Location? outerLocation, int offset = 0) + { + Text = text; + Source = source; + Start = start; + Length = text.Length; + Offset = offset; + OuterLocation = outerLocation; + } + + public string Text { get; } + public string Source { get; } + public int Start { get; } + public int Offset { get; } + public int Length { get; } + public Location? OuterLocation { get; } + + public bool IsImplicit + => Source == Implicit; + + public override string ToString() + => $"{(OuterLocation is null ? "" : OuterLocation.ToString() + "; ")}{Source} [{Start}, {Length}, {Offset}]"; + + } +} \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index bee1ab5ad1..9927aa4dd9 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -27,10 +29,13 @@ internal static int IndexOfCaseInsensitive( */ } - internal static class CliTokenizer + internal static class Tokenizer { + private const string doubleDash = "--"; + internal static (string? Prefix, string Alias) SplitPrefix(string rawAlias) { + // TODO: I believe this code would be faster and easier to understand with collection patterns if (rawAlias[0] == '/') { return ("/", rawAlias.Substring(1)); @@ -39,7 +44,7 @@ internal static (string? Prefix, string Alias) SplitPrefix(string rawAlias) { if (rawAlias.Length > 1 && rawAlias[1] == '-') { - return ("--", rawAlias.Substring(2)); + return (doubleDash, rawAlias.Substring(2)); } return ("-", rawAlias.Substring(1)); @@ -48,199 +53,171 @@ internal static (string? Prefix, string Alias) SplitPrefix(string rawAlias) return (null, rawAlias); } + // TODO: What does the following comment do, and do we need it // this method is not returning a Value Tuple or a dedicated type to avoid JITting + + // TODO: When would we ever not infer the rootcommand? This might have been to solve a bug where the first argument could not be the name of the root command. internal static void Tokenize( IReadOnlyList args, CliCommand rootCommand, + CliConfiguration configuration, bool inferRootCommand, - bool enablePosixBundling, out List tokens, out List? errors) { - const int FirstArgIsNotRootCommand = -1; - - List? errorList = null; - - var currentCommand = rootCommand; - var foundDoubleDash = false; - // TODO: Directives - /* - var foundEndOfDirectives = false; - */ - - var tokenList = new List(args.Count); + tokens = new List(args.Count); - var knownTokens = GetValidTokens(rootCommand); - - int i = FirstArgumentIsRootCommand(args, rootCommand, inferRootCommand) - ? 0 - : FirstArgIsNotRootCommand; - - for (; i < args.Count; i++) + // Handle exe not being in args + var rootIsExplicit = FirstArgIsRootCommand(args, rootCommand, inferRootCommand); + var rootLocation = Location.CreateRoot(rootCommand.Name, rootIsExplicit, rootIsExplicit ? 0 : -1); + if (!rootIsExplicit) // If it is explicit it will be added in the normal handling loop { - var arg = i == FirstArgIsNotRootCommand - ? rootCommand.Name - : args[i]; - - if (foundDoubleDash) - { - tokenList.Add(CommandArgument(arg, currentCommand!)); + tokens.Add(Command(rootCommand.Name, rootCommand, rootLocation)); + } - continue; - } + var maxSkippedPositions = configuration.PreProcessedLocations is null + || !configuration.PreProcessedLocations.Any() + ? 0 + : configuration.PreProcessedLocations.Max(x => x.Start); + + + var validTokens = GetValidTokens(rootCommand); + var newErrors = MapTokens(args, + rootLocation, + maxSkippedPositions, + rootCommand, + validTokens, + configuration, + false, + tokens); + + errors = newErrors; + + static List? MapTokens(IReadOnlyList args, + Location location, + int maxSkippedPositions, + CliCommand currentCommand, + Dictionary validTokens, + CliConfiguration configuration, + bool foundDoubleDash, + List tokens) + { + List? errors = null; + var previousOptionWasClosed = false; - if (!foundDoubleDash && - arg == "--") + for (var i = 0; i < args.Count; i++) { - tokenList.Add(DoubleDash()); - foundDoubleDash = true; - continue; - } + var arg = args[i]; - // TODO: Directives - /* - if (!foundEndOfDirectives) - { - if (arg.Length > 2 && - arg[0] == '[' && - arg[1] != ']' && - arg[1] != ':' && - arg[arg.Length - 1] == ']') + if (i <= maxSkippedPositions + && configuration.PreProcessedLocations is not null + && configuration.PreProcessedLocations.Any(x => x.Start == i)) { - int colonIndex = arg.AsSpan().IndexOf(':'); - string directiveName = colonIndex > 0 - ? arg.Substring(1, colonIndex - 1) // [name:value] - : arg.Substring(1, arg.Length - 2); // [name] is a legal directive - - CliDirective? directive; - if (knownTokens.TryGetValue($"[{directiveName}]", out var directiveToken)) - { - directive = (CliDirective)directiveToken.Symbol!; - } - else - { - directive = null; - } - - tokenList.Add(Directive(arg, directive)); continue; } - if (!configuration.RootCommand.EqualsNameOrAlias(arg)) + if (foundDoubleDash) { - foundEndOfDirectives = true; + // everything after the double dash is added as an argument + tokens.Add(CommandArgument(arg, currentCommand!, Location.FromOuterLocation(arg, i, location))); + continue; } - } - /* - // TODO: ResponseFileTokenReplacer - /* - if (configuration.ResponseFileTokenReplacer is { } replacer && - arg.GetReplaceableTokenValue() is { } value) - { - if (replacer( - value, - out var newTokens, - out var error)) + if (arg == doubleDash) { - if (newTokens is not null && newTokens.Count > 0) - { - List listWithReplacedTokens = args.ToList(); - listWithReplacedTokens.InsertRange(i + 1, newTokens); - args = listWithReplacedTokens; - } + tokens.Add(DoubleDash(i, Location.FromOuterLocation(arg, i, location))); + foundDoubleDash = true; continue; } - else if (!string.IsNullOrWhiteSpace(error)) + + // TODO: Figure out a place to put this test, or at least the prefix, somewhere not hard-coded + if (configuration.ResponseFileTokenReplacer is not null && + arg.StartsWith("@")) { - (errorList ??= new()).Add(error!); + var responseName = arg.Substring(1); + var (insertArgs, insertErrors) = configuration.ResponseFileTokenReplacer(responseName); + // TODO: Handle errors + if (insertArgs is not null && insertArgs.Any()) + { + var innerLocation = Location.CreateResponse(responseName, i, location); + var newErrors = MapTokens(insertArgs, innerLocation, 0, currentCommand, + validTokens, configuration, foundDoubleDash, tokens); + } continue; } - } - */ - if (knownTokens.TryGetValue(arg, out var token)) - { - if (PreviousTokenIsAnOptionExpectingAnArgument(out var option)) + if (TryGetSymbolAndTokenType(validTokens,arg, out var symbol, out var tokenType)) { - tokenList.Add(OptionArgument(arg, option!)); + // This test and block is to handle the case `-x -x` where -x takes a string arg and "-x" is the value. Normal + // option argument parsing is handled as all other arguments, because it is not a found token. + if (PreviousTokenIsAnOptionExpectingAnArgument(out var option, tokens, previousOptionWasClosed)) + { + tokens.Add(OptionArgument(arg, option!, Location.FromOuterLocation(arg, i, location))); + continue; + } + else + { + currentCommand = AddToken(currentCommand, tokens, ref validTokens, arg, + Location.FromOuterLocation(arg, i, location), tokenType, symbol); + previousOptionWasClosed = false; + } } else { - switch (token.Type) + if (TrySplitIntoSubtokens(arg, out var first, out var rest) && + TryGetSymbolAndTokenType(validTokens, first, out var subSymbol, out var subTokenType) && + subTokenType == CliTokenType.Option) { - case CliTokenType.Option: - tokenList.Add(Option(arg, (CliOption)token.Symbol!)); - break; + CliOption option = (CliOption)subSymbol!; + tokens.Add(Option(first, option, Location.FromOuterLocation(first, i, location))); - case CliTokenType.Command: - CliCommand cmd = (CliCommand)token.Symbol!; - if (cmd != currentCommand) - { - if (cmd != rootCommand) - { - knownTokens = GetValidTokens(cmd); // config contains Directives, they are allowed only for RootCommand - } - currentCommand = cmd; - tokenList.Add(Command(arg, cmd)); - } - else - { - tokenList.Add(Argument(arg)); - } - - break; + if (rest is not null) + { + tokens.Add(Argument(rest, Location.FromOuterLocation(rest, i, location, first.Length + 1))); + } + } + else if (!configuration.EnablePosixBundling || + !CanBeUnbundled(arg, tokens) || + !TryUnbundle(arg.AsSpan(1), Location.FromOuterLocation(arg, i, location), validTokens, tokens)) + { + tokens.Add(Argument(arg, Location.FromOuterLocation(arg, i, location))); } } } - else if (TrySplitIntoSubtokens(arg, out var first, out var rest) && - knownTokens.TryGetValue(first, out var subtoken) && - subtoken.Type == CliTokenType.Option) - { - tokenList.Add(Option(first, (CliOption)subtoken.Symbol!)); - if (rest is not null) - { - tokenList.Add(Argument(rest)); - } - } - else if (!enablePosixBundling || - !CanBeUnbundled(arg) || - !TryUnbundle(arg.AsSpan(1), i)) + return errors; + } + + static bool TryGetSymbolAndTokenType(Dictionary validTokens, + string arg, + [NotNullWhen(true)] out CliSymbol? symbol, + out CliTokenType tokenType) + { + if (validTokens.TryGetValue(arg, out var t)) { - tokenList.Add(Argument(arg)); + symbol = t.Symbol; + tokenType = t.TokenType; + return true; } - - CliToken Argument(string value) => new(value, CliTokenType.Argument, default, i); - - CliToken CommandArgument(string value, CliCommand command) => new(value, CliTokenType.Argument, command, i); - - CliToken OptionArgument(string value, CliOption option) => new(value, CliTokenType.Argument, option, i); - - CliToken Command(string value, CliCommand cmd) => new(value, CliTokenType.Command, cmd, i); - - CliToken Option(string value, CliOption option) => new(value, CliTokenType.Option, option, i); - - CliToken DoubleDash() => new("--", CliTokenType.DoubleDash, default, i); - - // TODO: Directives - // CliToken Directive(string value, CliDirective? directive) => new(value, CliTokenType.Directive, directive, i); + symbol = null; + tokenType = 0; + return false; } - tokens = tokenList; - errors = errorList; - - bool CanBeUnbundled(string arg) + static bool CanBeUnbundled(string arg, List tokenList) => arg.Length > 2 && arg[0] == '-' && arg[1] != '-'// don't check for "--" prefixed args && arg[2] != ':' && arg[2] != '=' // handled by TrySplitIntoSubtokens - && !PreviousTokenIsAnOptionExpectingAnArgument(out _); + && !PreviousTokenIsAnOptionExpectingAnArgument(out _, tokenList, false); - bool TryUnbundle(ReadOnlySpan alias, int argumentIndex) + static bool TryUnbundle(ReadOnlySpan alias, + Location outerLocation, + Dictionary validTokens, + List tokenList) { int tokensBefore = tokenList.Count; - + // TODO: Determine if these pointers are helping us enough for complexity. I do not see how it works, but changing it broke it. string candidate = new('-', 2); // mutable string used to avoid allocations unsafe { @@ -250,32 +227,40 @@ bool TryUnbundle(ReadOnlySpan alias, int argumentIndex) { if (alias[i] == ':' || alias[i] == '=') { - tokenList.Add(new CliToken(alias.Slice(i + 1).ToString(), CliTokenType.Argument, default, argumentIndex)); + string value = alias.Slice(i + 1).ToString(); + tokenList.Add(Argument(value, + Location.FromOuterLocation(value, outerLocation.Start, outerLocation, i + 1))); return true; } pCandidate[1] = alias[i]; - if (!knownTokens.TryGetValue(candidate, out CliToken? found)) + if (!validTokens.TryGetValue(candidate, out var found)) { if (tokensBefore != tokenList.Count && tokenList[tokenList.Count - 1].Type == CliTokenType.Option) { // Invalid_char_in_bundle_causes_rest_to_be_interpreted_as_value - tokenList.Add(new CliToken(alias.Slice(i).ToString(), CliTokenType.Argument, default, argumentIndex)); + string value = alias.Slice(i).ToString(); + tokenList.Add(Argument(value, + Location.FromOuterLocation(value, outerLocation.Start, outerLocation, i))); return true; } return false; } - tokenList.Add(new CliToken(found.Value, found.Type, found.Symbol, argumentIndex)); - if (i != alias.Length - 1 && ((CliOption)found.Symbol!).Greedy) + tokenList.Add(new CliToken(candidate, found.TokenType, found.Symbol, + Location.FromOuterLocation(candidate, outerLocation.Start, outerLocation, i + 1))); + + if (i != alias.Length - 1 && ((CliOption)found.Symbol).Greedy) { int index = i + 1; if (alias[index] == ':' || alias[index] == '=') { index++; // Last_bundled_option_can_accept_argument_with_colon_separator } - tokenList.Add(new CliToken(alias.Slice(index).ToString(), CliTokenType.Argument, default, argumentIndex)); + + string value = alias.Slice(index).ToString(); + tokenList.Add(Argument(value, Location.FromOuterLocation(value, outerLocation.Start, outerLocation, index))); return true; } } @@ -285,13 +270,13 @@ bool TryUnbundle(ReadOnlySpan alias, int argumentIndex) return true; } - bool PreviousTokenIsAnOptionExpectingAnArgument(out CliOption? option) + static bool PreviousTokenIsAnOptionExpectingAnArgument(out CliOption? option, List tokenList, bool previousOptionWasClosed) { if (tokenList.Count > 1) { var token = tokenList[tokenList.Count - 1]; - if (token.Type == CliTokenType.Option) + if (token.Type == CliTokenType.Option)// && !previousOptionWasClosed) { if (token.Symbol is CliOption { Greedy: true } opt) { @@ -304,9 +289,48 @@ bool PreviousTokenIsAnOptionExpectingAnArgument(out CliOption? option) option = null; return false; } + + static CliCommand AddToken(CliCommand currentCommand, + List tokenList, + ref Dictionary validTokens, + string arg, + Location location, + CliTokenType tokenType, + CliSymbol symbol) + { + //var location = Location.FromOuterLocation(outerLocation, argPosition, arg.Length); + switch (tokenType) + { + case CliTokenType.Option: + var option = (CliOption)symbol!; + tokenList.Add(Option(arg, option, location)); + break; + + case CliTokenType.Command: + // All arguments are initially classified as commands because they might be + CliCommand cmd = (CliCommand)symbol!; + if (cmd != currentCommand) + { + currentCommand = cmd; + // TODO: In the following determine how the cmd could be RootCommand AND the cmd not equal currentCmd. This looks like it would always be true.. If it is a massive side case, is it important not to double the ValidTokens call? + if (true) // cmd != rootCommand) + { + validTokens = GetValidTokens(cmd); // config contains Directives, they are allowed only for RootCommand + } + tokenList.Add(Command(arg, cmd, location)); + } + else + { + tokenList.Add(Argument(arg, location)); + } + + break; + } + return currentCommand; + } } - private static bool FirstArgumentIsRootCommand(IReadOnlyList args, CliCommand rootCommand, bool inferRootCommand) + private static bool FirstArgIsRootCommand(IReadOnlyList args, CliCommand rootCommand, bool inferRootCommand) { if (args.Count > 0) { @@ -333,11 +357,7 @@ private static bool FirstArgumentIsRootCommand(IReadOnlyList args, CliCo return false; } - private static string? GetReplaceableTokenValue(string arg) => - arg.Length > 1 && arg[0] == '@' - ? arg.Substring(1) - : null; - + // TODO: Naming rules - sub-tokens has a dash and thus should be SubToken private static bool TrySplitIntoSubtokens( string arg, out string first, @@ -362,87 +382,9 @@ private static bool TrySplitIntoSubtokens( return false; } - // TODO: rename to TryTokenizeResponseFile - internal static bool TryReadResponseFile( - string filePath, - out IReadOnlyList? newTokens, - out string? error) + private static Dictionary GetValidTokens(CliCommand command) { - try - { - newTokens = ExpandResponseFile(filePath).ToArray(); - error = null; - return true; - } - catch (FileNotFoundException) - { - error = LocalizationResources.ResponseFileNotFound(filePath); - } - catch (IOException e) - { - error = LocalizationResources.ErrorReadingResponseFile(filePath, e); - } - - newTokens = null; - return false; - - static IEnumerable ExpandResponseFile(string filePath) - { - var lines = File.ReadAllLines(filePath); - - for (var i = 0; i < lines.Length; i++) - { - var line = lines[i]; - - foreach (var p in SplitLine(line)) - { - if (GetReplaceableTokenValue(p) is { } path) - { - foreach (var q in ExpandResponseFile(path)) - { - yield return q; - } - } - else - { - yield return p; - } - } - } - } - - static IEnumerable SplitLine(string line) - { - var arg = line.Trim(); - - if (arg.Length == 0 || arg[0] == '#') - { - yield break; - } - - foreach (var word in CliParser.SplitCommandLine(arg)) - { - yield return word; - } - } - } - - private static Dictionary GetValidTokens(CliCommand command) - { - Dictionary tokens = new(StringComparer.Ordinal); - - // TODO: Directives - /* - if (command is CliRootCommand { Directives: IList directives }) - { - for (int i = 0; i < directives.Count; i++) - { - var directive = directives[i]; - var tokenString = $"[{directive.Name}]"; - tokens[tokenString] = new CliToken(tokenString, CliTokenType.Directive, directive, CliToken.ImplicitPosition); - } - } - */ + Dictionary tokens = new(StringComparer.Ordinal); AddCommandTokens(tokens, command); @@ -458,64 +400,34 @@ private static Dictionary GetValidTokens(CliCommand command) if (command.HasOptions) { var options = command.Options; - + for (int i = 0; i < options.Count; i++) { AddOptionTokens(tokens, options[i]); } } - CliCommand? current = command; - while (current is not null) - { - CliCommand? parentCommand = null; - SymbolNode? parent = current.FirstParent; - while (parent is not null) - { - if ((parentCommand = parent.Symbol as CliCommand) is not null) - { - if (parentCommand.HasOptions) - { - for (var i = 0; i < parentCommand.Options.Count; i++) - { - CliOption option = parentCommand.Options[i]; - // TODO: recursive options - /* - if (option.Recursive) - { - AddOptionTokens(tokens, option); - } - */ - } - } - - break; - } - parent = parent.Next; - } - current = parentCommand; - } - + // TODO: Be sure recursive/global options are handled in the Initialize of Help (add to all) return tokens; - static void AddCommandTokens(Dictionary tokens, CliCommand cmd) + static void AddCommandTokens(Dictionary tokens, CliCommand cmd) { - tokens.Add(cmd.Name, new CliToken(cmd.Name, CliTokenType.Command, cmd, CliToken.ImplicitPosition)); + tokens.Add(cmd.Name, (cmd, CliTokenType.Command)); if (cmd._aliases is not null) { foreach (string childAlias in cmd._aliases) { - tokens.Add(childAlias, new CliToken(childAlias, CliTokenType.Command, cmd, CliToken.ImplicitPosition)); + tokens.Add(childAlias, (cmd, CliTokenType.Command)); } } } - static void AddOptionTokens(Dictionary tokens, CliOption option) + static void AddOptionTokens(Dictionary tokens, CliOption option) { if (!tokens.ContainsKey(option.Name)) { - tokens.Add(option.Name, new CliToken(option.Name, CliTokenType.Option, option, CliToken.ImplicitPosition)); + tokens.Add(option.Name, (option, CliTokenType.Option)); } if (option._aliases is not null) @@ -524,11 +436,36 @@ static void AddOptionTokens(Dictionary tokens, CliOption optio { if (!tokens.ContainsKey(childAlias)) { - tokens.Add(childAlias, new CliToken(childAlias, CliTokenType.Option, option, CliToken.ImplicitPosition)); + tokens.Add(childAlias, (option, CliTokenType.Option)); } } } } } + + private static CliToken GetToken(string? value, CliTokenType tokenType, CliSymbol? symbol, Location location) + => new(value, tokenType, symbol, location); + + private static CliToken Argument(string arg, Location location) + => GetToken(arg, CliTokenType.Argument, default, location); + + private static CliToken CommandArgument(string arg, CliCommand command, Location location) + => GetToken(arg, CliTokenType.Argument, command, location); + + private static CliToken OptionArgument(string arg, CliOption option, Location location) + => GetToken(arg, CliTokenType.Argument, option, location); + + private static CliToken Command(string arg, CliCommand cmd, Location location) + => GetToken(arg, CliTokenType.Command, cmd, location); + + private static CliToken Option(string arg, CliOption option, Location location) + => GetToken(arg, CliTokenType.Option, option, location); + + // TODO: Explore whether double dash should track its command + private static CliToken DoubleDash(int i, Location location) + => GetToken(doubleDash, CliTokenType.DoubleDash, default, location); + } + + } \ No newline at end of file diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 4595ebb603..fa596533f6 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -51,6 +51,7 @@ + From 83934e09ac208716a3c31afb91f8dfb27b95993b Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 9 Mar 2024 17:39:27 -0500 Subject: [PATCH 040/150] Refactor subsystem initialization The configuration and args are now wrapped in an InitializationContext --- .../AlternateSubsystems.cs | 4 +- .../ErrorReportingSubsystem.cs | 2 +- .../HelpSubsystem.cs | 9 ++-- src/System.CommandLine.Subsystems/Pipeline.cs | 46 +++++++++---------- .../Subsystems/CliSubsystem.cs | 6 +-- .../Subsystems/InitializationContext.cs | 10 ++++ .../Subsystems/Subsystem.cs | 12 ++--- .../VersionSubsystem.cs | 9 ++-- 8 files changed, 54 insertions(+), 44 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/Subsystems/InitializationContext.cs diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index 9ae61750ac..a92e99cb6a 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -45,11 +45,11 @@ internal class VersionWithInitializeAndTeardown : VersionSubsystem internal bool ExecutionWasRun; internal bool TeardownWasRun; - protected override CliConfiguration Initialize(CliConfiguration configuration) + protected override CliConfiguration Initialize(InitializationContext context) { // marker hack needed because ConsoleHack not available in initialization InitializationWasRun = true; - return base.Initialize(configuration); + return base.Initialize(context); } protected override CliExit Execute(PipelineContext pipelineContext) diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs index 9ded0c298e..bb14fddb1d 100644 --- a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -9,7 +9,7 @@ namespace System.CommandLine; public class ErrorReportingSubsystem : CliSubsystem { public ErrorReportingSubsystem(IAnnotationProvider? annotationProvider = null) - : base(ErrorReportingAnnotations.Prefix, annotationProvider, SubsystemKind.ErrorReporting) + : base(ErrorReportingAnnotations.Prefix, SubsystemKind.ErrorReporting, annotationProvider) { } // TODO: Stash option rather than using string diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index 9fc8c15997..f332e9a0ae 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -16,7 +16,7 @@ namespace System.CommandLine; // .With(help.Description, "Greet the user"); // public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) - : CliSubsystem(HelpAnnotations.Prefix, annotationProvider: annotationProvider, SubsystemKind.Help) + : CliSubsystem(HelpAnnotations.Prefix, SubsystemKind.Help, annotationProvider) { public void SetDescription(CliSymbol symbol, string description) => SetAnnotation(symbol, HelpAnnotations.Description, description); @@ -29,15 +29,16 @@ public string GetDescription(CliSymbol symbol) public AnnotationAccessor Description => new(this, HelpAnnotations.Description); - protected internal override CliConfiguration Initialize(CliConfiguration configuration) + protected internal override CliConfiguration Initialize(InitializationContext context) { var option = new CliOption("--help", ["-h"]) { + // TODO: Why don't we accept bool like any other bool option? Arity = ArgumentArity.Zero }; - configuration.RootCommand.Add(option); + context.Configuration.RootCommand.Add(option); - return configuration; + return context.Configuration; } protected internal override bool GetIsActivated(ParseResult? parseResult) diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index b8250cdb5a..3dc4f59781 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -16,9 +16,9 @@ public class Pipeline public ParseResult Parse(CliConfiguration configuration, string rawInput) => Parse(configuration, CliParser.SplitCommandLine(rawInput).ToArray()); - public ParseResult Parse(CliConfiguration configuration, string[] args) + public ParseResult Parse(CliConfiguration configuration, IReadOnlyList args) { - InitializeSubsystems(configuration); + InitializeSubsystems(new InitializationContext(configuration, args)); var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); return parseResult; } @@ -39,17 +39,17 @@ public CliExit Execute(ParseResult parseResult, string rawInput, ConsoleHack? co return new CliExit(pipelineContext); } - protected virtual void InitializeHelp(CliConfiguration configuration) - => Help?.Initialize(configuration); + protected virtual void InitializeHelp(InitializationContext context) + => Help?.Initialize(context); - protected virtual void InitializeVersion(CliConfiguration configuration) - => Version?.Initialize(configuration); + protected virtual void InitializeVersion(InitializationContext context) + => Version?.Initialize(context); - protected virtual void InitializeErrorReporting(CliConfiguration configuration) - => ErrorReporting?.Initialize(configuration); + protected virtual void InitializeCompletion(InitializationContext context) + => Completion?.Initialize(context); - protected virtual void InitializeCompletion(CliConfiguration configuration) - => Completion?.Initialize(configuration); + protected virtual void InitializeErrorReporting(InitializationContext context) + => ErrorReporting?.Initialize(context); protected virtual CliExit TearDownHelp(CliExit cliExit) => Help is null @@ -61,28 +61,28 @@ protected virtual CliExit TearDownHelp(CliExit cliExit) ? cliExit : Version.TearDown(cliExit); + protected virtual CliExit TearDownCompletion(CliExit cliExit) + => Completion is null + ? cliExit + : Completion.TearDown(cliExit); + protected virtual CliExit TearDownErrorReporting(CliExit cliExit) => ErrorReporting is null ? cliExit : ErrorReporting.TearDown(cliExit); - protected virtual CliExit TearDownCompletions(CliExit cliExit) - => Completion is null - ? cliExit - : Completion.TearDown(cliExit); - protected virtual void ExecuteHelp(PipelineContext context) => ExecuteIfNeeded(Help, context); protected virtual void ExecuteVersion(PipelineContext context) => ExecuteIfNeeded(Version, context); + protected virtual void ExecuteCompletion(PipelineContext context) + => ExecuteIfNeeded(Completion, context); + protected virtual void ExecuteErrorReporting(PipelineContext context) => ExecuteIfNeeded(ErrorReporting, context); - protected virtual void ExecuteCompletions(PipelineContext context) - => ExecuteIfNeeded(Completion, context); - // TODO: Consider whether this should be public. It would simplify testing, but would it do anything else // TODO: Confirm that it is OK for ConsoleHack to be unavailable in Initialize /// @@ -94,12 +94,12 @@ protected virtual void ExecuteCompletions(PipelineContext context) /// /// Note to inheritors: The ordering of initializing should normally be in the reverse order than tear down /// - protected virtual void InitializeSubsystems(CliConfiguration configuration) + protected virtual void InitializeSubsystems(InitializationContext context) { - InitializeHelp(configuration); - InitializeVersion(configuration); - InitializeErrorReporting(configuration); - InitializeCompletion(configuration); + InitializeHelp(context); + InitializeVersion(context); + InitializeCompletion(context); + InitializeErrorReporting(context); } // TODO: Consider whether this should be public diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 794eff661d..ec1ede69a4 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.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.CommandLine.Subsystems.Annotations; using System.Diagnostics.CodeAnalysis; namespace System.CommandLine.Subsystems; @@ -12,7 +11,7 @@ namespace System.CommandLine.Subsystems; /// public abstract class CliSubsystem { - protected CliSubsystem(string name, IAnnotationProvider? annotationProvider, SubsystemKind subsystemKind) + protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProvider? annotationProvider) { Name = name; _annotationProvider = annotationProvider; @@ -115,7 +114,8 @@ internal PipelineContext ExecuteIfNeeded(ParseResult? parseResult, PipelineConte /// The CLI configuration, which contains the RootCommand for customization /// True if parsing should continue // there might be a better design that supports a message // TODO: Because of this and similar usage, consider combining CLI declaration and config. ArgParse calls this the parser, which I like - protected internal virtual CliConfiguration Initialize(CliConfiguration configuration) => configuration; + protected internal virtual CliConfiguration Initialize(InitializationContext context) + => context.Configuration; // TODO: Determine if this is needed. protected internal virtual CliExit TearDown(CliExit cliExit) diff --git a/src/System.CommandLine.Subsystems/Subsystems/InitializationContext.cs b/src/System.CommandLine.Subsystems/Subsystems/InitializationContext.cs new file mode 100644 index 0000000000..0a8f1ed80e --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/InitializationContext.cs @@ -0,0 +1,10 @@ +// 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.Subsystems; + +public class InitializationContext(CliConfiguration configuration, IReadOnlyList args) +{ + public CliConfiguration Configuration { get; } = configuration; + public IReadOnlyList Args { get; } = args; +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs index a61ab23ef2..f67f5ecdc4 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs @@ -5,8 +5,8 @@ namespace System.CommandLine.Subsystems; public class Subsystem { - public static void Initialize(CliSubsystem subsystem, CliConfiguration configuration) - => subsystem.Initialize(configuration); + public static void Initialize(CliSubsystem subsystem, CliConfiguration configuration, IReadOnlyList args) + => subsystem.Initialize(new InitializationContext(configuration, args)); public static CliExit Execute(CliSubsystem subsystem, PipelineContext pipelineContext) => subsystem.Execute(pipelineContext); @@ -14,15 +14,15 @@ public static CliExit Execute(CliSubsystem subsystem, PipelineContext pipelineCo public static bool GetIsActivated(CliSubsystem subsystem, ParseResult parseResult) => subsystem.GetIsActivated(parseResult); - public static CliExit ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) - => new(subsystem.ExecuteIfNeeded(new PipelineContext(parseResult, rawInput, null,consoleHack))); + public static CliExit ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) + => new(subsystem.ExecuteIfNeeded(new PipelineContext(parseResult, rawInput, null, consoleHack))); public static CliExit Execute(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) => subsystem.Execute(new PipelineContext(parseResult, rawInput, null, consoleHack)); - internal static PipelineContext ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack, PipelineContext? pipelineContext = null) - => subsystem.ExecuteIfNeeded(pipelineContext ?? new PipelineContext(parseResult, rawInput, null,consoleHack)); + internal static PipelineContext ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack, PipelineContext? pipelineContext = null) + => subsystem.ExecuteIfNeeded(pipelineContext ?? new PipelineContext(parseResult, rawInput, null, consoleHack)); internal static PipelineContext ExecuteIfNeeded(CliSubsystem subsystem, PipelineContext pipelineContext) => subsystem.ExecuteIfNeeded(pipelineContext); diff --git a/src/System.CommandLine.Subsystems/VersionSubsystem.cs b/src/System.CommandLine.Subsystems/VersionSubsystem.cs index 0740e3bc6e..ee763fa55a 100644 --- a/src/System.CommandLine.Subsystems/VersionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/VersionSubsystem.cs @@ -12,7 +12,7 @@ public class VersionSubsystem : CliSubsystem private string? specificVersion = null; public VersionSubsystem(IAnnotationProvider? annotationProvider = null) - : base(VersionAnnotations.Prefix, annotationProvider, SubsystemKind.Version) + : base(VersionAnnotations.Prefix, SubsystemKind.Version, annotationProvider) { } @@ -34,16 +34,15 @@ public string? SpecificVersion ?.GetCustomAttribute() ?.InformationalVersion; - - protected internal override CliConfiguration Initialize(CliConfiguration configuration) + protected internal override CliConfiguration Initialize(InitializationContext context) { var option = new CliOption("--version", ["-v"]) { Arity = ArgumentArity.Zero }; - configuration.RootCommand.Add(option); + context.Configuration.RootCommand.Add(option); - return configuration; + return context.Configuration; } // TODO: Stash option rather than using string From 0746536c1ff2e891d02aa19b0887f998fa10317b Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Wed, 3 Apr 2024 23:53:59 -0400 Subject: [PATCH 041/150] Clean up pipeline/subsystem tests --- .../PipelineTests.cs | 165 +++++++++--------- ...System.CommandLine.Subsystems.Tests.csproj | 1 + .../TestData.cs | 33 ++++ .../VersionSubsystemTests.cs | 20 +-- .../CompletionSubsystem.cs | 2 +- 5 files changed, 129 insertions(+), 92 deletions(-) create mode 100644 src/System.CommandLine.Subsystems.Tests/TestData.cs diff --git a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs index 297590108e..198376544e 100644 --- a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs @@ -3,87 +3,78 @@ using FluentAssertions; using System.CommandLine.Parsing; -using System.Reflection; using Xunit; namespace System.CommandLine.Subsystems.Tests { public class PipelineTests { - - private static readonly string? version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) - ?.GetCustomAttribute() - ?.InformationalVersion; - + private static Pipeline GetTestPipeline(VersionSubsystem versionSubsystem) + => new() + { + Version = versionSubsystem + }; + private static CliConfiguration GetNewTestConfiguration() + => new(new CliRootCommand { new CliOption("-x") }); // Add option expected by test data + + private static ConsoleHack GetNewTestConsole() + => new ConsoleHack().RedirectToBuffer(true); + + //private static (Pipeline pipeline, CliConfiguration configuration, ConsoleHack consoleHack) StandardObjects(VersionSubsystem versionSubsystem) + //{ + // var configuration = new CliConfiguration(new CliRootCommand { new CliOption("-x") }); + // var pipeline = new Pipeline + // { + // Version = versionSubsystem + // }; + // var consoleHack = new ConsoleHack().RedirectToBuffer(true); + // return (pipeline, configuration, consoleHack); + //} [Theory] - [InlineData("-v", true)] - [InlineData("--version", true)] - [InlineData("-x", false)] - [InlineData("", false)] - [InlineData(null, false)] + [ClassData(typeof(TestData.Version))] public void Subsystem_runs_in_pipeline_only_when_requested(string input, bool shouldRun) { - var configuration = new CliConfiguration(new CliRootCommand { }); - var pipeline = new Pipeline - { - Version = new VersionSubsystem() - }; - var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var pipeline = GetTestPipeline(new VersionSubsystem()); + var console = GetNewTestConsole(); - var exit = pipeline.Execute(configuration, input, consoleHack); + var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); exit.ExitCode.Should().Be(0); exit.Handled.Should().Be(shouldRun); if (shouldRun) { - consoleHack.GetBuffer().Trim().Should().Be(version); + console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); } } [Theory] - [InlineData("-v", true)] - [InlineData("--version", true)] - [InlineData("-x", false)] - [InlineData("", false)] - [InlineData(null, false)] + [ClassData(typeof(TestData.Version))] public void Subsystem_runs_with_explicit_parse_only_when_requested(string input, bool shouldRun) { - var configuration = new CliConfiguration(new CliRootCommand { }); - var pipeline = new Pipeline - { - Version = new VersionSubsystem() - }; - var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var pipeline = GetTestPipeline(new VersionSubsystem()); + var console = GetNewTestConsole(); - var result = pipeline.Parse(configuration, input); - var exit = pipeline.Execute(result, input, consoleHack); + var result = pipeline.Parse(GetNewTestConfiguration(), input); + var exit = pipeline.Execute(result, input, console); exit.ExitCode.Should().Be(0); exit.Handled.Should().Be(shouldRun); if (shouldRun) { - consoleHack.GetBuffer().Trim().Should().Be(version); + console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); } } [Theory] - [InlineData("-v", true)] - [InlineData("--version", true)] - [InlineData("-x", false)] - [InlineData("", false)] - [InlineData(null, false)] + [ClassData(typeof(TestData.Version))] public void Subsystem_runs_initialize_and_teardown_when_requested(string input, bool shouldRun) { - var configuration = new CliConfiguration(new CliRootCommand { }); - AlternateSubsystems.VersionWithInitializeAndTeardown versionSubsystem = new AlternateSubsystems.VersionWithInitializeAndTeardown(); - var pipeline = new Pipeline - { - Version = versionSubsystem - }; - var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new AlternateSubsystems.VersionWithInitializeAndTeardown(); + var pipeline = GetTestPipeline(versionSubsystem); + var console = GetNewTestConsole(); - var exit = pipeline.Execute(configuration, input, consoleHack); + var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); exit.ExitCode.Should().Be(0); exit.Handled.Should().Be(shouldRun); @@ -94,56 +85,72 @@ public void Subsystem_runs_initialize_and_teardown_when_requested(string input, [Theory] - [InlineData("-v", true)] - [InlineData("--version", true)] - [InlineData("-x", false)] - [InlineData("", false)] - [InlineData(null, false)] - public void Subsystem_can_be_used_without_runner(string input, bool shouldRun) + [ClassData(typeof(TestData.Version))] + public void Subsystem_works_without_pipeline(string input, bool shouldRun) { - var configuration = new CliConfiguration(new CliRootCommand { }); var versionSubsystem = new VersionSubsystem(); - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - - Subsystem.Initialize(versionSubsystem, configuration); - // TODO: I do not know why anyone would do this, but I do not see a reason to work to block it. See style2 below - var parseResult = CliParser.Parse(configuration.RootCommand, input, configuration); + // TODO: Ensure an efficient conversion as people may copy this code + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + var console = GetNewTestConsole(); + var configuration = GetNewTestConfiguration(); + + Subsystem.Initialize(versionSubsystem, configuration, args); + // This approach might be taken if someone is using a subsystem just for initialization + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); bool value = parseResult.GetValue("--version"); + parseResult.Errors.Should().BeEmpty(); value.Should().Be(shouldRun); - if (shouldRun) + if (shouldRun) { // TODO: Add an execute overload to avoid checking activated twice - var exit = Subsystem.Execute(versionSubsystem, parseResult, input, consoleHack); + var exit = Subsystem.Execute(versionSubsystem, parseResult, input, console); exit.Should().NotBeNull(); exit.ExitCode.Should().Be(0); exit.Handled.Should().BeTrue(); - consoleHack.GetBuffer().Trim().Should().Be(version); + console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); } } [Theory] - [InlineData("-v", true)] - [InlineData("--version", true)] - [InlineData("-x", false)] - [InlineData("", false)] - [InlineData(null, false)] - public void Subsystem_can_be_used_without_runner_style2(string input, bool shouldRun) + [ClassData(typeof(TestData.Version))] + public void Subsystem_works_without_pipeline_style2(string input, bool shouldRun) { - var configuration = new CliConfiguration(new CliRootCommand { }); var versionSubsystem = new VersionSubsystem(); - var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + var console = GetNewTestConsole(); + var configuration = GetNewTestConfiguration(); var expectedVersion = shouldRun - ? version + ? TestData.AssemblyVersionString : ""; - Subsystem.Initialize(versionSubsystem, configuration); - var parseResult = CliParser.Parse(configuration.RootCommand, input, configuration); - var exit = Subsystem.ExecuteIfNeeded(versionSubsystem, parseResult, input, consoleHack); + // Someone might use this approach if they wanted to do something with the ParseResult + Subsystem.Initialize(versionSubsystem, configuration, args); + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); + var exit = Subsystem.ExecuteIfNeeded(versionSubsystem, parseResult, input, console); exit.ExitCode.Should().Be(0); exit.Handled.Should().Be(shouldRun); - consoleHack.GetBuffer().Trim().Should().Be(expectedVersion); + console.GetBuffer().Trim().Should().Be(expectedVersion); + } + + + [Theory] + [InlineData("-xy", false)] + [InlineData("--versionx", false)] + public void Subsystem_runs_when_requested_even_when_there_are_errors(string input, bool shouldRun) + { + var versionSubsystem = new VersionSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + var configuration = GetNewTestConfiguration(); + + Subsystem.Initialize(versionSubsystem, configuration, args); + // This approach might be taken if someone is using a subsystem just for initialization + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); + bool value = parseResult.GetValue("--version"); + + parseResult.Errors.Should().NotBeEmpty(); + value.Should().Be(shouldRun); } [Fact] @@ -171,9 +178,8 @@ public void Normal_pipeline_contains_no_subsystems() public void Subsystems_can_access_each_others_data() { // TODO: Explore a mechanism that doesn't require the reference to retrieve data, this shows that it is awkward - var consoleHack = new ConsoleHack().RedirectToBuffer(true); var symbol = new CliOption("-x"); - + var console = GetNewTestConsole(); var pipeline = new StandardPipeline { Version = new AlternateSubsystems.VersionThatUsesHelpData(symbol) @@ -183,9 +189,10 @@ public void Subsystems_can_access_each_others_data() { symbol.With(pipeline.Help.Description, "Testing") }; - pipeline.Execute(new CliConfiguration(rootCommand), "-v", consoleHack); - consoleHack.GetBuffer().Trim().Should().Be($"Testing"); - } + pipeline.Execute(new CliConfiguration(rootCommand), "-v", console); + + console.GetBuffer().Trim().Should().Be($"Testing"); + } } } diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index f93b8e94b9..aff5a953fd 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -32,6 +32,7 @@ + diff --git a/src/System.CommandLine.Subsystems.Tests/TestData.cs b/src/System.CommandLine.Subsystems.Tests/TestData.cs new file mode 100644 index 0000000000..0ae908ac6b --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/TestData.cs @@ -0,0 +1,33 @@ +// 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; +using System.Reflection; + +namespace System.CommandLine.Subsystems.Tests; + +internal class TestData +{ + internal static readonly string? AssemblyVersionString = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) + ?.GetCustomAttribute() + ?.InformationalVersion; + + internal class Version : IEnumerable + { + // This data only works if the CLI has a --version with a -v alias and also has a -x option + private readonly List _data = + [ + ["--version", true], + ["-v", true], + ["-vx", true], + ["-xv", true], + ["-x", false], + [null, false], + ["", false], + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs index 6cecb7beb9..304c12b6da 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs @@ -1,23 +1,21 @@ // 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.Reflection; using FluentAssertions; using Xunit; using System.CommandLine.Parsing; namespace System.CommandLine.Subsystems.Tests { - public class VersionSubsystemTests { [Fact] public void When_version_subsystem_is_used_the_version_option_is_added_to_the_root() { var rootCommand = new CliRootCommand - { - new CliOption("-x") - }; + { + new CliOption("-x") // add option that is expected for the test data used here + }; var configuration = new CliConfiguration(rootCommand); var pipeline = new Pipeline { @@ -32,20 +30,18 @@ public void When_version_subsystem_is_used_the_version_option_is_added_to_the_ro .Count(x => x.Name == "--version") .Should() .Be(1); - } [Theory] - [InlineData("--version", true)] - [InlineData("-v", true)] - [InlineData("-x", false)] - [InlineData("", false)] + [ClassData(typeof(TestData.Version))] public void Version_is_activated_only_when_requested(string input, bool result) { - CliRootCommand rootCommand = new(); + CliRootCommand rootCommand = [new CliOption("-x")]; // add random option as empty CLIs are rare var configuration = new CliConfiguration(rootCommand); var versionSubsystem = new VersionSubsystem(); - Subsystem.Initialize(versionSubsystem, configuration); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + + Subsystem.Initialize(versionSubsystem, configuration, args); var parseResult = CliParser.Parse(rootCommand, input, configuration); var isActive = Subsystem.GetIsActivated(versionSubsystem, parseResult); diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index bc024ca886..adae591c3a 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -9,7 +9,7 @@ namespace System.CommandLine; public class CompletionSubsystem : CliSubsystem { public CompletionSubsystem(IAnnotationProvider? annotationProvider = null) - : base(CompletionAnnotations.Prefix, annotationProvider, SubsystemKind.Completion) + : base(CompletionAnnotations.Prefix, SubsystemKind.Completion, annotationProvider) { } // TODO: Figure out trigger for completions From 8f7918df8e6bad56bb518fe1359f27ac43e9f598 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Thu, 4 Apr 2024 00:18:32 -0400 Subject: [PATCH 042/150] Add abstract DirectiveSubsystem The core parser no longer supports the special "directive" syntax, but this abstract class provides a reusable implementation of handling directive syntax in a subsystem using preprocessing. --- .../AlternateSubsystems.cs | 10 ++++ .../DirectiveSubsystemTests.cs | 41 +++++++++++++ ...System.CommandLine.Subsystems.Tests.csproj | 1 + .../TestData.cs | 25 ++++++++ .../Directives/DirectiveSubsystem.cs | 60 +++++++++++++++++++ 5 files changed, 137 insertions(+) create mode 100644 src/System.CommandLine.Subsystems.Tests/DirectiveSubsystemTests.cs create mode 100644 src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index a92e99cb6a..b0dc5b5dbd 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -1,7 +1,9 @@ // 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.Directives; using System.CommandLine.Subsystems; +using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine.Subsystems.Tests { @@ -65,5 +67,13 @@ protected override CliExit TearDown(CliExit cliExit) } } + internal class StringDirectiveSubsystem(IAnnotationProvider? annotationProvider = null) + : DirectiveSubsystem("other",SubsystemKind.Other, annotationProvider) + { } + + internal class BooleanDirectiveSubsystem(IAnnotationProvider? annotationProvider = null) + : DirectiveSubsystem("diagram", SubsystemKind.Other, annotationProvider) + { } + } } diff --git a/src/System.CommandLine.Subsystems.Tests/DirectiveSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/DirectiveSubsystemTests.cs new file mode 100644 index 0000000000..2dbdebb506 --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/DirectiveSubsystemTests.cs @@ -0,0 +1,41 @@ +// 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 FluentAssertions; +using System.CommandLine.Directives; +using System.CommandLine.Parsing; +using Xunit; + +namespace System.CommandLine.Subsystems.Tests; + +public class DirectiveSubsystemTests +{ + + // For Boolean tests see DiagramSubsystemTests + + [Theory] + [ClassData(typeof(TestData.Directive))] + // TODO: Not sure why these tests are passing + public void String_directive_supplies_string_or_default_and_is_activated_only_when_requested( + string input, bool expectedBoolIsActive, bool expectedStringIsActive, string? expectedValue) + { + CliRootCommand rootCommand = [new CliCommand("x")]; + var configuration = new CliConfiguration(rootCommand); + var stringSubsystem = new AlternateSubsystems.StringDirectiveSubsystem(); + var boolSubsystem = new AlternateSubsystems.BooleanDirectiveSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + + Subsystem.Initialize(stringSubsystem, configuration, args); + Subsystem.Initialize(boolSubsystem, configuration, args); + + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var stringIsActive = Subsystem.GetIsActivated(stringSubsystem, parseResult); + var boolIsActive = Subsystem.GetIsActivated(boolSubsystem, parseResult); + var actualValue = stringSubsystem.Value; + + boolIsActive.Should().Be(expectedBoolIsActive); + stringIsActive.Should().Be(expectedStringIsActive); + actualValue.Should().Be(expectedValue); + + } +} diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index aff5a953fd..8fa4a4f75e 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -31,6 +31,7 @@ --> + diff --git a/src/System.CommandLine.Subsystems.Tests/TestData.cs b/src/System.CommandLine.Subsystems.Tests/TestData.cs index 0ae908ac6b..1e41341f35 100644 --- a/src/System.CommandLine.Subsystems.Tests/TestData.cs +++ b/src/System.CommandLine.Subsystems.Tests/TestData.cs @@ -30,4 +30,29 @@ internal class Version : IEnumerable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + internal class Directive : IEnumerable + { + private readonly List _data = + [ + ["[diagram]", true, false, null], + ["[other:Hello]", false, true, "Hello"], + ["[diagram] x", true, false, null], + ["[diagram] -o", true, false, null], + ["[diagram] -v", true, false, null], + ["[diagram] x -v", true, false, null], + ["[diagramX]", false, false, null], + ["[diagram] [other:Hello]", true, true, "Hello"], + ["x", false, false, null], + ["-o", false, false, null], + ["x -x", false, false, null], + [null, false, false, null], + ["", false, false, null], + //["[diagram] [other Goodbye]", true, true, "Goodbye"],This is a new test that demos new feature, but is also broken + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } diff --git a/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs new file mode 100644 index 0000000000..6b4b7d9670 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs @@ -0,0 +1,60 @@ +// 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.Parsing; +using System.CommandLine.Subsystems; + +namespace System.CommandLine.Directives; + +public abstract class DirectiveSubsystem : CliSubsystem +{ + public string? Value { get; private set; } + public bool Found { get; private set; } + public string Id { get; } + public Location? Location { get; private set; } + + public DirectiveSubsystem(string name, SubsystemKind kind, IAnnotationProvider? annotationProvider = null, string? id = null) + : base(name, kind, annotationProvider: annotationProvider) + { + Id = id ?? name; + } + + protected internal override CliConfiguration Initialize(InitializationContext context) + { + for (int i = 0; i < context.Args.Count; i++) + { + var arg = context.Args[i]; + if (arg[0] == '[') // It looks like a directive, see if it is the one we want + { + var start = arg.IndexOf($"[{Id}"); + // Protect against matching substrings, such as "diagramX" matching "diagram" - but longer string may be valid for a different directive and we may still find the one we want + if (start >= 0) + { + var end = arg.IndexOf("]", start) + 1; + var nextChar = arg[start + Id.Length + 1]; + if (nextChar is ']' or ':') + { + Found = true; + if (nextChar == ':') + { + Value = arg[(start + Id.Length + 2)..(end - 1)]; + } + Location = new Location(arg.Substring(start, end - start), Location.User, i, null, start); + context.Configuration.AddPreprocessedLocation(Location); + break; + } + } + } + else if (i > 0) // First position might be ExeName, but directives are not legal after other tokens appear + { + break; + } + } + + return context.Configuration; + } + + protected internal override bool GetIsActivated(ParseResult? parseResult) + => Found; + +} From 9d3cbed717dd7b3607e58714d9ada631d6244e6e Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Thu, 4 Apr 2024 00:20:19 -0400 Subject: [PATCH 043/150] Add diagram subsystem and tests --- .../DiagramSubsystemTests.cs | 45 +++++ ...System.CommandLine.Subsystems.Tests.csproj | 1 + .../TestData.cs | 24 +++ .../Directives/DiagramSubsystem.cs | 178 ++++++++++++++++++ src/System.CommandLine.Subsystems/Pipeline.cs | 24 ++- .../StandardPipeline.cs | 7 +- .../Annotations/DiagramAnnotations.cs | 13 ++ .../Subsystems/SubsystemKind.cs | 1 + 8 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 src/System.CommandLine.Subsystems.Tests/DiagramSubsystemTests.cs create mode 100644 src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/DiagramAnnotations.cs diff --git a/src/System.CommandLine.Subsystems.Tests/DiagramSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/DiagramSubsystemTests.cs new file mode 100644 index 0000000000..254526dab0 --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/DiagramSubsystemTests.cs @@ -0,0 +1,45 @@ +// 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 FluentAssertions; +using System.CommandLine.Directives; +using System.CommandLine.Parsing; +using Xunit; + +namespace System.CommandLine.Subsystems.Tests; + +public class DiagramSubsystemTests +{ + + [Theory] + [ClassData(typeof(TestData.Diagram))] + public void Diagram_is_activated_only_when_requested(string input, bool expectedIsActive) + { + CliRootCommand rootCommand = [new CliCommand("x")]; + var configuration = new CliConfiguration(rootCommand); + var subsystem = new DiagramSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + + Subsystem.Initialize(subsystem, configuration, args); + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var isActive = Subsystem.GetIsActivated(subsystem, parseResult); + + isActive.Should().Be(expectedIsActive); + } + + [Theory] + [ClassData(typeof(TestData.Diagram))] + public void String_directive_supplies_string_or_default_and_is_activated_only_when_requested(string input, bool expectedIsActive) + { + CliRootCommand rootCommand = [new CliCommand("x")]; + var configuration = new CliConfiguration(rootCommand); + var subsystem = new DiagramSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + + Subsystem.Initialize(subsystem, configuration, args); + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var isActive = Subsystem.GetIsActivated(subsystem, parseResult); + + isActive.Should().Be(expectedIsActive); + } +} diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index 8fa4a4f75e..a3cc42f498 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -32,6 +32,7 @@ + diff --git a/src/System.CommandLine.Subsystems.Tests/TestData.cs b/src/System.CommandLine.Subsystems.Tests/TestData.cs index 1e41341f35..0460bb1d33 100644 --- a/src/System.CommandLine.Subsystems.Tests/TestData.cs +++ b/src/System.CommandLine.Subsystems.Tests/TestData.cs @@ -31,6 +31,30 @@ internal class Version : IEnumerable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + internal class Diagram : IEnumerable + { + // The tests define an x command, but -o and -v are just random values + private readonly List _data = + [ + ["[diagram]", true], + ["[diagram] x", true], + ["[diagram] -o", true], + ["[diagram] -v", true], + ["[diagram] x -v", true], + ["[diagramX]", false], + ["[diagram] [other]", true], + ["x", false], + ["-o", false], + ["x -x", false], + [null, false], + ["", false] + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + internal class Directive : IEnumerable { private readonly List _data = diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs new file mode 100644 index 0000000000..e9051cc6c1 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -0,0 +1,178 @@ +// 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.Subsystems; +using System.Text; +using System.CommandLine.Parsing; + +namespace System.CommandLine.Directives; + +public class DiagramSubsystem( IAnnotationProvider? annotationProvider = null) + : DirectiveSubsystem("diagram", SubsystemKind.Diagram, annotationProvider) +{ + //protected internal override bool GetIsActivated(ParseResult? parseResult) + // => parseResult is not null && option is not null && parseResult.GetValue(option); + + protected internal override CliExit Execute(PipelineContext pipelineContext) + { + // Gather locations + //var locations = pipelineContext.ParseResult.LocationMap + // .Concat(Map(pipelineContext.ParseResult.Configuration.PreProcessedLocations)); + + pipelineContext.ConsoleHack.WriteLine("Output diagram"); + return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + } + + + // TODO: Capture logic in previous diagramming, shown below + /// + /// Formats a string explaining a parse result. + /// + /// The parse result to be diagrammed. + /// A string containing a diagram of the parse result. + internal static StringBuilder Diagram(ParseResult parseResult) + { + var builder = new StringBuilder(100); + + + Diagram(builder, parseResult.RootCommandResult, parseResult); + + // TODO: Unmatched tokens + /* + var unmatchedTokens = parseResult.UnmatchedTokens; + if (unmatchedTokens.Count > 0) + { + builder.Append(" ???-->"); + + for (var i = 0; i < unmatchedTokens.Count; i++) + { + var error = unmatchedTokens[i]; + builder.Append(' '); + builder.Append(error); + } + } + */ + + return builder; + } + + private static void Diagram( + StringBuilder builder, + SymbolResult symbolResult, + ParseResult parseResult) + { + if (parseResult.Errors.Any(e => e.SymbolResult == symbolResult)) + { + builder.Append('!'); + } + +/* + switch (symbolResult) + { + // TODO: Directives + case DirectiveResult { Directive: not DiagramDirective }: + break; + + // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives + case ArgumentResult argumentResult: + { + var includeArgumentName = + argumentResult.Argument.FirstParent!.Symbol is CliCommand { HasArguments: true, Arguments.Count: > 1 }; + + if (includeArgumentName) + { + builder.Append("[ "); + builder.Append(argumentResult.Argument.Name); + builder.Append(' '); + } + + if (argumentResult.Argument.Arity.MaximumNumberOfValues > 0) + { + ArgumentConversionResult conversionResult = argumentResult.GetArgumentConversionResult(); + switch (conversionResult.Result) + { + case ArgumentConversionResultType.NoArgument: + break; + case ArgumentConversionResultType.Successful: + switch (conversionResult.Value) + { + case string s: + builder.Append($"<{s}>"); + break; + + case IEnumerable items: + builder.Append('<'); + builder.Append( + string.Join("> <", + items.Cast().ToArray())); + builder.Append('>'); + break; + + default: + builder.Append('<'); + builder.Append(conversionResult.Value); + builder.Append('>'); + break; + } + + break; + + default: // failures + builder.Append('<'); + builder.Append(string.Join("> <", symbolResult.Tokens.Select(t => t.Value))); + builder.Append('>'); + + break; + } + } + + if (includeArgumentName) + { + builder.Append(" ]"); + } + + break; + } + + default: + { + OptionResult? optionResult = symbolResult as OptionResult; + + if (optionResult is { Implicit: true }) + { + builder.Append('*'); + } + + builder.Append("[ "); + + if (optionResult is not null) + { + builder.Append(optionResult.IdentifierToken?.Value ?? optionResult.Option.Name); + } + else + { + builder.Append(((CommandResult)symbolResult).IdentifierToken.Value); + } + + foreach (SymbolResult child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) + { + if (child is ArgumentResult arg && + (arg.Argument.ValueType == typeof(bool) || + arg.Argument.Arity.MaximumNumberOfValues == 0)) + { + continue; + } + + builder.Append(' '); + + Diagram(builder, child, parseResult); + } + + builder.Append(" ]"); + break; + } + } + } +*/ + } +} diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 3dc4f59781..ec7aea776a 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -1,6 +1,7 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// 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.Directives; using System.CommandLine.Parsing; using System.CommandLine.Subsystems; @@ -10,8 +11,9 @@ public class Pipeline { public HelpSubsystem? Help { get; set; } public VersionSubsystem? Version { get; set; } - public ErrorReportingSubsystem? ErrorReporting { get; set; } public CompletionSubsystem? Completion { get; set; } + public DiagramSubsystem? Diagram { get; set; } + public ErrorReportingSubsystem? ErrorReporting { get; set; } public ParseResult Parse(CliConfiguration configuration, string rawInput) => Parse(configuration, CliParser.SplitCommandLine(rawInput).ToArray()); @@ -48,6 +50,9 @@ protected virtual void InitializeVersion(InitializationContext context) protected virtual void InitializeCompletion(InitializationContext context) => Completion?.Initialize(context); + protected virtual void InitializeDiagram(InitializationContext context) + => Diagram?.Initialize(context); + protected virtual void InitializeErrorReporting(InitializationContext context) => ErrorReporting?.Initialize(context); @@ -66,6 +71,11 @@ protected virtual CliExit TearDownCompletion(CliExit cliExit) ? cliExit : Completion.TearDown(cliExit); + protected virtual CliExit TearDownDiagram(CliExit cliExit) + => Diagram is null + ? cliExit + : Diagram.TearDown(cliExit); + protected virtual CliExit TearDownErrorReporting(CliExit cliExit) => ErrorReporting is null ? cliExit @@ -80,6 +90,9 @@ protected virtual void ExecuteVersion(PipelineContext context) protected virtual void ExecuteCompletion(PipelineContext context) => ExecuteIfNeeded(Completion, context); + protected virtual void ExecuteDiagram(PipelineContext context) + => ExecuteIfNeeded(Diagram, context); + protected virtual void ExecuteErrorReporting(PipelineContext context) => ExecuteIfNeeded(ErrorReporting, context); @@ -99,6 +112,7 @@ protected virtual void InitializeSubsystems(InitializationContext context) InitializeHelp(context); InitializeVersion(context); InitializeCompletion(context); + InitializeDiagram(context); InitializeErrorReporting(context); } @@ -113,8 +127,9 @@ protected virtual void InitializeSubsystems(InitializationContext context) /// protected virtual CliExit TearDownSubsystems(CliExit cliExit) { - TearDownCompletions(cliExit); TearDownErrorReporting(cliExit); + TearDownDiagram(cliExit); + TearDownCompletion(cliExit); TearDownVersion(cliExit); TearDownHelp(cliExit); return cliExit; @@ -124,8 +139,9 @@ protected virtual void ExecuteSubsystems(PipelineContext pipelineContext) { ExecuteHelp(pipelineContext); ExecuteVersion(pipelineContext); + ExecuteCompletion(pipelineContext); + ExecuteDiagram(pipelineContext); ExecuteErrorReporting(pipelineContext); - ExecuteCompletions(pipelineContext); } protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineContext pipelineContext) diff --git a/src/System.CommandLine.Subsystems/StandardPipeline.cs b/src/System.CommandLine.Subsystems/StandardPipeline.cs index d1ab7a65cd..6e5792ebfe 100644 --- a/src/System.CommandLine.Subsystems/StandardPipeline.cs +++ b/src/System.CommandLine.Subsystems/StandardPipeline.cs @@ -1,14 +1,17 @@ // 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.Directives; + namespace System.CommandLine; public class StandardPipeline : Pipeline -{ +{ public StandardPipeline() { Help = new HelpSubsystem(); Version = new VersionSubsystem(); - ErrorReporting = new ErrorReportingSubsystem(); Completion = new CompletionSubsystem(); + Diagram = new DiagramSubsystem(); + ErrorReporting = new ErrorReportingSubsystem(); } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/DiagramAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/DiagramAnnotations.cs new file mode 100644 index 0000000000..dc4ea181fc --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/DiagramAnnotations.cs @@ -0,0 +1,13 @@ +// 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.Subsystems.Annotations; + +/// +/// IDs for well-known diagram annotations. +/// +public static class DiagramAnnotations +{ + public static string Prefix { get; } = nameof(SubsystemKind.Diagram); + +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs index 8e6b80f3b2..a7ecf22f7d 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs @@ -10,4 +10,5 @@ public enum SubsystemKind Version, ErrorReporting, Completion, + Diagram, } From 60f527252ad327ab064cc781d197f32a08688ea6 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Thu, 4 Apr 2024 00:21:13 -0400 Subject: [PATCH 044/150] Add subsystem implementing response file handling --- .../ResponseSubsystemTests.cs | 31 ++++++ .../Response_1.rsp | 1 + ...System.CommandLine.Subsystems.Tests.csproj | 2 + .../Directives/ResponseSubsystem.cs | 103 ++++++++++++++++++ .../Subsystems/SubsystemKind.cs | 1 + 5 files changed, 138 insertions(+) create mode 100644 src/System.CommandLine.Subsystems.Tests/ResponseSubsystemTests.cs create mode 100644 src/System.CommandLine.Subsystems.Tests/Response_1.rsp create mode 100644 src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs diff --git a/src/System.CommandLine.Subsystems.Tests/ResponseSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ResponseSubsystemTests.cs new file mode 100644 index 0000000000..363a576e71 --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/ResponseSubsystemTests.cs @@ -0,0 +1,31 @@ +// 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 FluentAssertions; +using System.CommandLine.Directives; +using System.CommandLine.Parsing; +using Xunit; + +namespace System.CommandLine.Subsystems.Tests; + +public class ResponseSubsystemTests +{ + + [Fact] + // TODO: Not sure why these tests are passing + public void Simple_response_file_contributes_to_parsing() + { + var option = new CliOption("--hello"); + var rootCommand = new CliRootCommand { option }; + var configuration = new CliConfiguration(rootCommand); + var subsystem = new ResponseSubsystem(); + string[] args = ["@Response_1.rsp"]; + + Subsystem.Initialize(subsystem, configuration, args); + + var parseResult = CliParser.Parse(rootCommand, args, configuration); + var value = parseResult.GetValue(option); + + value.Should().Be("world"); + } +} diff --git a/src/System.CommandLine.Subsystems.Tests/Response_1.rsp b/src/System.CommandLine.Subsystems.Tests/Response_1.rsp new file mode 100644 index 0000000000..93f169504f --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/Response_1.rsp @@ -0,0 +1 @@ +--hello world diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index a3cc42f498..7af1fa8222 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -16,6 +16,7 @@ + @@ -31,6 +32,7 @@ --> + diff --git a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs new file mode 100644 index 0000000000..ed43c8d626 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs @@ -0,0 +1,103 @@ +// 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.Parsing; +using System.CommandLine.Subsystems; + +namespace System.CommandLine.Directives; + +public class ResponseSubsystem() + : CliSubsystem("Response", SubsystemKind.Response, null) +{ + protected internal override CliConfiguration Initialize(InitializationContext context) + { + context.Configuration.ResponseFileTokenReplacer = Replacer; + return context.Configuration; + } + + public static (List? tokens, List? errors) Replacer(string responseSourceName) + { + try + { + // TODO: Include checks from previous system. + var contents = File.ReadAllText(responseSourceName); + return (CliParser.SplitCommandLine(contents).ToList(), null); + } + catch + { + // TODO: Switch to proper errors + return (null, + errors: + [ + $"Failed to open response file {responseSourceName}" + ]); + } + } + + // TODO: File handling from previous system - ensure these checks are done (note: no tests caught these oversights + /* internal static bool TryReadResponseFile( + string filePath, + out IReadOnlyList? newTokens, + out string? error) + { + try + { + newTokens = ExpandResponseFile(filePath).ToArray(); + error = null; + return true; + } + catch (FileNotFoundException) + { + error = LocalizationResources.ResponseFileNotFound(filePath); + } + catch (IOException e) + { + error = LocalizationResources.ErrorReadingResponseFile(filePath, e); + } + + newTokens = null; + return false; + + static IEnumerable ExpandResponseFile(string filePath) + { + var lines = File.ReadAllLines(filePath); + + for (var i = 0; i < lines.Length; i++) + { + var line = lines[i]; + + foreach (var p in SplitLine(line)) + { + if (GetReplaceableTokenValue(p) is { } path) + { + foreach (var q in ExpandResponseFile(path)) + { + yield return q; + } + } + else + { + yield return p; + } + } + } + } + + static IEnumerable SplitLine(string line) + { + var arg = line.Trim(); + + if (arg.Length == 0 || arg[0] == '#') + { + yield break; + } + + foreach (var word in CliParser.SplitCommandLine(arg)) + { + yield return word; + } + } + } + */ + +} \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs index a7ecf22f7d..5ad2bfbb66 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs @@ -11,4 +11,5 @@ public enum SubsystemKind ErrorReporting, Completion, Diagram, + Response, } From 06fa079877f93b01b67a7c34be1a0890b0a8c8e2 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 4 Apr 2024 00:45:20 -0400 Subject: [PATCH 045/150] Fix build on osx-arm64 --- .../EndToEndTestApp/EndToEndTestApp.csproj | 2 +- src/System.CommandLine.Suggest/dotnet-suggest.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/EndToEndTestApp.csproj b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/EndToEndTestApp.csproj index a10ab84566..b41dabb7c2 100644 --- a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/EndToEndTestApp.csproj +++ b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/EndToEndTestApp.csproj @@ -7,7 +7,7 @@ Exe $(TargetFrameworkForNETSDK) - win-x64;linux-x64;osx-x64 + win-x64;linux-x64;osx-x64;osx-arm64 diff --git a/src/System.CommandLine.Suggest/dotnet-suggest.csproj b/src/System.CommandLine.Suggest/dotnet-suggest.csproj index f0ae84d57d..1676a14195 100644 --- a/src/System.CommandLine.Suggest/dotnet-suggest.csproj +++ b/src/System.CommandLine.Suggest/dotnet-suggest.csproj @@ -7,7 +7,7 @@ true dotnet-suggest dotnet-suggest - win-x64;win-x86;osx-x64;linux-x64 + win-x64;win-x86;osx-x64;linux-x64;osx-arm64 $(OutputPath) .1 From 204a997f48724e797e6459c5576da7de94ea3b3a Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 9 Mar 2024 17:39:27 -0500 Subject: [PATCH 046/150] ValueResult unifies OptionResult and ArgumentResult. OptionResult and ArgumentResult access internal data and ValueResult is a public face for the data and location. --- .../AlternateSubsystems.cs | 4 +- .../PipelineTests.cs | 322 ++-- .../VersionSubsystemTests.cs | 197 +- .../Directives/DiagramSubsystem.cs | 6 +- .../Directives/ResponseSubsystem.cs | 2 +- .../Annotations/ValueAnnotations.cs | 17 + .../Subsystems/CliSubsystem.cs | 2 +- .../Subsystems/SubsystemKind.cs | 3 +- .../ValueSubsystem.cs | 42 + src/System.CommandLine.Tests/ParserTests.cs | 1608 ++++++++++------- .../TokenizerTests.cs | 9 +- src/System.CommandLine/ArgumentArity.cs | 6 +- src/System.CommandLine/ParseResult.cs | 9 + .../Parsing/ArgumentResult.cs | 67 +- src/System.CommandLine/Parsing/CliToken.cs | 1 - .../Parsing/CommandResult.cs | 17 +- .../Parsing/CommandValueResult.cs | 17 + src/System.CommandLine/Parsing/Location.cs | 2 +- .../Parsing/OptionResult.cs | 18 + .../Parsing/ParseOperation.cs | 2 + .../Parsing/StringExtensions.cs | 2 +- .../Parsing/SymbolResultTree.cs | 92 +- src/System.CommandLine/Parsing/ValueResult.cs | 61 + .../Parsing/ValueResultExtensions.cs | 17 + .../Parsing/ValueResultOutcome.cs | 11 + .../System.CommandLine.csproj | 4 + 26 files changed, 1526 insertions(+), 1012 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs create mode 100644 src/System.CommandLine.Subsystems/ValueSubsystem.cs create mode 100644 src/System.CommandLine/Parsing/CommandValueResult.cs create mode 100644 src/System.CommandLine/Parsing/ValueResult.cs create mode 100644 src/System.CommandLine/Parsing/ValueResultExtensions.cs create mode 100644 src/System.CommandLine/Parsing/ValueResultOutcome.cs diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index b0dc5b5dbd..b3ae527fe0 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -68,11 +68,11 @@ protected override CliExit TearDown(CliExit cliExit) } internal class StringDirectiveSubsystem(IAnnotationProvider? annotationProvider = null) - : DirectiveSubsystem("other",SubsystemKind.Other, annotationProvider) + : DirectiveSubsystem("other",SubsystemKind.Diagram, annotationProvider) { } internal class BooleanDirectiveSubsystem(IAnnotationProvider? annotationProvider = null) - : DirectiveSubsystem("diagram", SubsystemKind.Other, annotationProvider) + : DirectiveSubsystem("diagram", SubsystemKind.Diagram, annotationProvider) { } } diff --git a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs index 198376544e..2c14cc0de4 100644 --- a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs @@ -5,194 +5,192 @@ using System.CommandLine.Parsing; using Xunit; -namespace System.CommandLine.Subsystems.Tests +namespace System.CommandLine.Subsystems.Tests; + +public class PipelineTests { - public class PipelineTests - { - private static Pipeline GetTestPipeline(VersionSubsystem versionSubsystem) - => new() - { - Version = versionSubsystem - }; - private static CliConfiguration GetNewTestConfiguration() - => new(new CliRootCommand { new CliOption("-x") }); // Add option expected by test data - - private static ConsoleHack GetNewTestConsole() - => new ConsoleHack().RedirectToBuffer(true); - - //private static (Pipeline pipeline, CliConfiguration configuration, ConsoleHack consoleHack) StandardObjects(VersionSubsystem versionSubsystem) - //{ - // var configuration = new CliConfiguration(new CliRootCommand { new CliOption("-x") }); - // var pipeline = new Pipeline - // { - // Version = versionSubsystem - // }; - // var consoleHack = new ConsoleHack().RedirectToBuffer(true); - // return (pipeline, configuration, consoleHack); - //} - - [Theory] - [ClassData(typeof(TestData.Version))] - public void Subsystem_runs_in_pipeline_only_when_requested(string input, bool shouldRun) + private static Pipeline GetTestPipeline(VersionSubsystem versionSubsystem) + => new() { - var pipeline = GetTestPipeline(new VersionSubsystem()); - var console = GetNewTestConsole(); + Version = versionSubsystem + }; + private static CliConfiguration GetNewTestConfiguration() + => new(new CliRootCommand { new CliOption("-x") }); // Add option expected by test data + + private static ConsoleHack GetNewTestConsole() + => new ConsoleHack().RedirectToBuffer(true); + + //private static (Pipeline pipeline, CliConfiguration configuration, ConsoleHack consoleHack) StandardObjects(VersionSubsystem versionSubsystem) + //{ + // var configuration = new CliConfiguration(new CliRootCommand { new CliOption("-x") }); + // var pipeline = new Pipeline + // { + // Version = versionSubsystem + // }; + // var consoleHack = new ConsoleHack().RedirectToBuffer(true); + // return (pipeline, configuration, consoleHack); + //} + + [Theory] + [ClassData(typeof(TestData.Version))] + public void Subsystem_runs_in_pipeline_only_when_requested(string input, bool shouldRun) + { + var pipeline = GetTestPipeline(new VersionSubsystem()); + var console = GetNewTestConsole(); - var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); + var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); - exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); - if (shouldRun) - { - console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); - } + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + if (shouldRun) + { + console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); } + } - [Theory] - [ClassData(typeof(TestData.Version))] - public void Subsystem_runs_with_explicit_parse_only_when_requested(string input, bool shouldRun) - { - var pipeline = GetTestPipeline(new VersionSubsystem()); - var console = GetNewTestConsole(); + [Theory] + [ClassData(typeof(TestData.Version))] + public void Subsystem_runs_with_explicit_parse_only_when_requested(string input, bool shouldRun) + { + var pipeline = GetTestPipeline(new VersionSubsystem()); + var console = GetNewTestConsole(); - var result = pipeline.Parse(GetNewTestConfiguration(), input); - var exit = pipeline.Execute(result, input, console); + var result = pipeline.Parse(GetNewTestConfiguration(), input); + var exit = pipeline.Execute(result, input, console); - exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); - if (shouldRun) - { - console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); - } + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + if (shouldRun) + { + console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); } + } - [Theory] - [ClassData(typeof(TestData.Version))] - public void Subsystem_runs_initialize_and_teardown_when_requested(string input, bool shouldRun) - { - var versionSubsystem = new AlternateSubsystems.VersionWithInitializeAndTeardown(); - var pipeline = GetTestPipeline(versionSubsystem); - var console = GetNewTestConsole(); + [Theory] + [ClassData(typeof(TestData.Version))] + public void Subsystem_runs_initialize_and_teardown_when_requested(string input, bool shouldRun) + { + var versionSubsystem = new AlternateSubsystems.VersionWithInitializeAndTeardown(); + var pipeline = GetTestPipeline(versionSubsystem); + var console = GetNewTestConsole(); - var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); + var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); - exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); - versionSubsystem.InitializationWasRun.Should().BeTrue(); - versionSubsystem.ExecutionWasRun.Should().Be(shouldRun); - versionSubsystem.TeardownWasRun.Should().BeTrue(); - } + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + versionSubsystem.InitializationWasRun.Should().BeTrue(); + versionSubsystem.ExecutionWasRun.Should().Be(shouldRun); + versionSubsystem.TeardownWasRun.Should().BeTrue(); + } - [Theory] - [ClassData(typeof(TestData.Version))] - public void Subsystem_works_without_pipeline(string input, bool shouldRun) + [Theory] + [ClassData(typeof(TestData.Version))] + public void Subsystem_works_without_pipeline(string input, bool shouldRun) + { + var versionSubsystem = new VersionSubsystem(); + // TODO: Ensure an efficient conversion as people may copy this code + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + var console = GetNewTestConsole(); + var configuration = GetNewTestConfiguration(); + + Subsystem.Initialize(versionSubsystem, configuration, args); + // This approach might be taken if someone is using a subsystem just for initialization + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); + bool value = parseResult.GetValue("--version"); + + parseResult.Errors.Should().BeEmpty(); + value.Should().Be(shouldRun); + if (shouldRun) { - var versionSubsystem = new VersionSubsystem(); - // TODO: Ensure an efficient conversion as people may copy this code - var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); - var console = GetNewTestConsole(); - var configuration = GetNewTestConfiguration(); - - Subsystem.Initialize(versionSubsystem, configuration, args); - // This approach might be taken if someone is using a subsystem just for initialization - var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); - bool value = parseResult.GetValue("--version"); - - parseResult.Errors.Should().BeEmpty(); - value.Should().Be(shouldRun); - if (shouldRun) - { - // TODO: Add an execute overload to avoid checking activated twice - var exit = Subsystem.Execute(versionSubsystem, parseResult, input, console); - exit.Should().NotBeNull(); - exit.ExitCode.Should().Be(0); - exit.Handled.Should().BeTrue(); - console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); - } + // TODO: Add an execute overload to avoid checking activated twice + var exit = Subsystem.Execute(versionSubsystem, parseResult, input, console); + exit.Should().NotBeNull(); + exit.ExitCode.Should().Be(0); + exit.Handled.Should().BeTrue(); + console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); } + } - [Theory] - [ClassData(typeof(TestData.Version))] - public void Subsystem_works_without_pipeline_style2(string input, bool shouldRun) - { - var versionSubsystem = new VersionSubsystem(); - var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); - var console = GetNewTestConsole(); - var configuration = GetNewTestConfiguration(); - var expectedVersion = shouldRun - ? TestData.AssemblyVersionString - : ""; - - // Someone might use this approach if they wanted to do something with the ParseResult - Subsystem.Initialize(versionSubsystem, configuration, args); - var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); - var exit = Subsystem.ExecuteIfNeeded(versionSubsystem, parseResult, input, console); + [Theory] + [ClassData(typeof(TestData.Version))] + public void Subsystem_works_without_pipeline_style2(string input, bool shouldRun) + { + var versionSubsystem = new VersionSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + var console = GetNewTestConsole(); + var configuration = GetNewTestConfiguration(); + var expectedVersion = shouldRun + ? TestData.AssemblyVersionString + : ""; + + // Someone might use this approach if they wanted to do something with the ParseResult + Subsystem.Initialize(versionSubsystem, configuration, args); + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); + var exit = Subsystem.ExecuteIfNeeded(versionSubsystem, parseResult, input, console); + + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + console.GetBuffer().Trim().Should().Be(expectedVersion); + } - exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); - console.GetBuffer().Trim().Should().Be(expectedVersion); - } + [Theory] + [InlineData("-xy", false)] + [InlineData("--versionx", false)] + public void Subsystem_runs_when_requested_even_when_there_are_errors(string input, bool shouldRun) + { + var versionSubsystem = new VersionSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + var configuration = GetNewTestConfiguration(); - [Theory] - [InlineData("-xy", false)] - [InlineData("--versionx", false)] - public void Subsystem_runs_when_requested_even_when_there_are_errors(string input, bool shouldRun) - { - var versionSubsystem = new VersionSubsystem(); - var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); - var configuration = GetNewTestConfiguration(); + Subsystem.Initialize(versionSubsystem, configuration, args); + // This approach might be taken if someone is using a subsystem just for initialization + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); + bool value = parseResult.GetValue("--version"); - Subsystem.Initialize(versionSubsystem, configuration, args); - // This approach might be taken if someone is using a subsystem just for initialization - var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); - bool value = parseResult.GetValue("--version"); + parseResult.Errors.Should().NotBeEmpty(); + value.Should().Be(shouldRun); + } - parseResult.Errors.Should().NotBeEmpty(); - value.Should().Be(shouldRun); - } + [Fact] + public void Standard_pipeline_contains_expected_subsystems() + { + var pipeline = new StandardPipeline(); + pipeline.Version.Should().BeOfType(); + pipeline.Help.Should().BeOfType(); + pipeline.ErrorReporting.Should().BeOfType(); + pipeline.Completion.Should().BeOfType(); + } - [Fact] - public void Standard_pipeline_contains_expected_subsystems() - { - var pipeline = new StandardPipeline(); - pipeline.Version.Should().BeOfType(); - pipeline.Help.Should().BeOfType(); - pipeline.ErrorReporting.Should().BeOfType(); - pipeline.Completion.Should().BeOfType(); - } + [Fact] + public void Normal_pipeline_contains_no_subsystems() + { + var pipeline = new Pipeline(); + pipeline.Version.Should().BeNull(); + pipeline.Help.Should().BeNull(); + pipeline.ErrorReporting.Should().BeNull(); + pipeline.Completion.Should().BeNull(); + } - [Fact] - public void Normal_pipeline_contains_no_subsystems() + [Fact] + public void Subsystems_can_access_each_others_data() + { + // TODO: Explore a mechanism that doesn't require the reference to retrieve data, this shows that it is awkward + var symbol = new CliOption("-x"); + var console = GetNewTestConsole(); + var pipeline = new StandardPipeline { - var pipeline = new Pipeline(); - pipeline.Version.Should().BeNull(); - pipeline.Help.Should().BeNull(); - pipeline.ErrorReporting.Should().BeNull(); - pipeline.Completion.Should().BeNull(); - } + Version = new AlternateSubsystems.VersionThatUsesHelpData(symbol) + }; + if (pipeline.Help is null) throw new InvalidOperationException(); + var rootCommand = new CliRootCommand + { + symbol.With(pipeline.Help.Description, "Testing") + }; + pipeline.Execute(new CliConfiguration(rootCommand), "-v", console); - [Fact] - public void Subsystems_can_access_each_others_data() - { - // TODO: Explore a mechanism that doesn't require the reference to retrieve data, this shows that it is awkward - var symbol = new CliOption("-x"); - var console = GetNewTestConsole(); - var pipeline = new StandardPipeline - { - Version = new AlternateSubsystems.VersionThatUsesHelpData(symbol) - }; - if (pipeline.Help is null) throw new InvalidOperationException(); - var rootCommand = new CliRootCommand - { - symbol.With(pipeline.Help.Description, "Testing") - }; - - pipeline.Execute(new CliConfiguration(rootCommand), "-v", console); - - console.GetBuffer().Trim().Should().Be($"Testing"); - } + console.GetBuffer().Trim().Should().Be($"Testing"); } } diff --git a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs index 304c12b6da..8472cd2bb8 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs @@ -5,117 +5,116 @@ using Xunit; using System.CommandLine.Parsing; -namespace System.CommandLine.Subsystems.Tests +namespace System.CommandLine.Subsystems.Tests; + +public class VersionSubsystemTests { - public class VersionSubsystemTests + [Fact] + public void When_version_subsystem_is_used_the_version_option_is_added_to_the_root() { - [Fact] - public void When_version_subsystem_is_used_the_version_option_is_added_to_the_root() - { - var rootCommand = new CliRootCommand - { - new CliOption("-x") // add option that is expected for the test data used here - }; - var configuration = new CliConfiguration(rootCommand); - var pipeline = new Pipeline - { - Version = new VersionSubsystem() - }; - - // Parse is used because directly calling Initialize would be unusual - var result = pipeline.Parse(configuration, ""); - - rootCommand.Options.Should().NotBeNull(); - rootCommand.Options - .Count(x => x.Name == "--version") - .Should() - .Be(1); - } - - [Theory] - [ClassData(typeof(TestData.Version))] - public void Version_is_activated_only_when_requested(string input, bool result) + var rootCommand = new CliRootCommand + { + new CliOption("-x") // add option that is expected for the test data used here + }; + var configuration = new CliConfiguration(rootCommand); + var pipeline = new Pipeline { - CliRootCommand rootCommand = [new CliOption("-x")]; // add random option as empty CLIs are rare - var configuration = new CliConfiguration(rootCommand); - var versionSubsystem = new VersionSubsystem(); - var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + Version = new VersionSubsystem() + }; - Subsystem.Initialize(versionSubsystem, configuration, args); + // Parse is used because directly calling Initialize would be unusual + var result = pipeline.Parse(configuration, ""); - var parseResult = CliParser.Parse(rootCommand, input, configuration); - var isActive = Subsystem.GetIsActivated(versionSubsystem, parseResult); + rootCommand.Options.Should().NotBeNull(); + rootCommand.Options + .Count(x => x.Name == "--version") + .Should() + .Be(1); + } - isActive.Should().Be(result); - } + [Theory] + [ClassData(typeof(TestData.Version))] + public void Version_is_activated_only_when_requested(string input, bool result) + { + CliRootCommand rootCommand = [new CliOption("-x")]; // add random option as empty CLIs are rare + var configuration = new CliConfiguration(rootCommand); + var versionSubsystem = new VersionSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); - [Fact] - public void Outputs_assembly_version() - { - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var versionSubsystem = new VersionSubsystem(); - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be(Constants.version); - } - - [Fact] - public void Outputs_specified_version() - { - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var versionSubsystem = new VersionSubsystem - { - SpecificVersion = "42" - }; - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be("42"); - } - - [Fact] - public void Outputs_assembly_version_when_specified_version_set_to_null() + Subsystem.Initialize(versionSubsystem, configuration, args); + + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var isActive = Subsystem.GetIsActivated(versionSubsystem, parseResult); + + isActive.Should().Be(result); + } + + [Fact] + public void Outputs_assembly_version() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem(); + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be(Constants.version); + } + + [Fact] + public void Outputs_specified_version() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem { - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var versionSubsystem = new VersionSubsystem - { - SpecificVersion = null - }; - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be(Constants.version); - } - - [Fact] - public void Console_output_can_be_tested() + SpecificVersion = "42" + }; + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be("42"); + } + + [Fact] + public void Outputs_assembly_version_when_specified_version_set_to_null() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem { - CliConfiguration configuration = new(new CliRootCommand()) - { }; + SpecificVersion = null + }; + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be(Constants.version); + } + + [Fact] + public void Console_output_can_be_tested() + { + CliConfiguration configuration = new(new CliRootCommand()) + { }; - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var versionSubsystem = new VersionSubsystem(); - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be(Constants.version); - } + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem(); + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be(Constants.version); + } - [Fact] - public void Custom_version_subsystem_can_be_used() + [Fact] + public void Custom_version_subsystem_can_be_used() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var pipeline = new Pipeline { - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var pipeline = new Pipeline - { - Version = new AlternateSubsystems.AlternateVersion() - }; - pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); - consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); - } - - [Fact] - public void Custom_version_subsystem_can_replace_standard() + Version = new AlternateSubsystems.AlternateVersion() + }; + pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); + consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); + } + + [Fact] + public void Custom_version_subsystem_can_replace_standard() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var pipeline = new StandardPipeline { - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var pipeline = new StandardPipeline - { - Version = new AlternateSubsystems.AlternateVersion() - }; - pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); - consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); - } + Version = new AlternateSubsystems.AlternateVersion() + }; + pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); + consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); } } diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index e9051cc6c1..9702108ffd 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -66,14 +66,16 @@ private static void Diagram( builder.Append('!'); } -/* switch (symbolResult) { // TODO: Directives + /* case DirectiveResult { Directive: not DiagramDirective }: break; + */ // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives + /* case ArgumentResult argumentResult: { var includeArgumentName = @@ -172,7 +174,7 @@ private static void Diagram( break; } } + */ } -*/ } } diff --git a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs index ed43c8d626..7ccfe817c2 100644 --- a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs @@ -26,7 +26,7 @@ public static (List? tokens, List? errors) Replacer(string respo catch { // TODO: Switch to proper errors - return (null, + return (null, errors: [ $"Failed to open response file {responseSourceName}" diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs new file mode 100644 index 0000000000..b9a04ad7ad --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs @@ -0,0 +1,17 @@ +// 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.Parsing; + +namespace System.CommandLine.Subsystems.Annotations; + +/// +/// IDs for well-known Version annotations. +/// +public static class ValueAnnotations +{ + public static string Prefix { get; } = nameof(SubsystemKind.Value); + + public static AnnotationId Explicit { get; } = new(Prefix, nameof(Explicit)); + public static AnnotationId> Calculated { get; } = new(Prefix, nameof(Calculated)); +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index ec1ede69a4..6f3e70461e 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -62,7 +62,7 @@ protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId< /// An out parameter to contain the result /// /// This value is protected because these values are always retrieved from derived classes that offer - /// strongly typed explicit methods, such as help having `GAetDescription(Symbol symbol, "My help descrption")` method. + /// strongly typed explicit methods, such as help having `GetDescription(Symbol symbol, "My help description")` method. /// protected internal void SetAnnotation(CliSymbol symbol, AnnotationId id, TValue value) { diff --git a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs index 5ad2bfbb66..8962392915 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs @@ -8,8 +8,9 @@ public enum SubsystemKind Other = 0, Help, Version, + Value, ErrorReporting, Completion, Diagram, - Response, + Response } diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs new file mode 100644 index 0000000000..62955cd8d6 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -0,0 +1,42 @@ +// 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.Parsing; +using System.CommandLine.Subsystems; +using System.CommandLine.Subsystems.Annotations; + +namespace System.CommandLine; + +public class ValueSubsystem : CliSubsystem +{ + private ParseResult? parseResult = null; + + public ValueSubsystem(IAnnotationProvider? annotationProvider = null) + : base(ValueAnnotations.Prefix, SubsystemKind.Version, annotationProvider) + { } + + void SetExplicit(CliSymbol symbol, object value) + => SetAnnotation(symbol, ValueAnnotations.Explicit, value); + object GetExplicit(CliSymbol symbol) + => TryGetAnnotation(symbol, ValueAnnotations.Explicit, out var value) + ? value + : ""; + AnnotationAccessor Explicit + => new(this, ValueAnnotations.Explicit); + + void SetCalculated(CliSymbol symbol, Func factory) + => SetAnnotation(symbol, ValueAnnotations.Calculated, factory); + Func GetCalculated(CliSymbol symbol) + => TryGetAnnotation>(symbol, ValueAnnotations.Calculated, out var value) + ? value + : null; + AnnotationAccessor> Calculated + => new(this, ValueAnnotations.Calculated); + + protected internal override bool GetIsActivated(ParseResult? parseResult) + { + this.parseResult = parseResult; + return true; + } +} + diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 3da60b50a6..4cdaa9b91d 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -72,9 +72,9 @@ public void When_a_token_is_just_a_prefix_then_an_error_is_returned(string prefi var result = CliParser.Parse(rootCommand, prefix); result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument(prefix)); + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument(prefix)); } [Fact] @@ -109,7 +109,7 @@ public void Short_form_options_can_be_specified_using_colon_delimiter() var option = new CliOption("-x"); var rootCommand = new CliRootCommand { option }; - var result = CliParser.Parse(rootCommand,"-x:some-value"); + var result = CliParser.Parse(rootCommand, "-x:some-value"); result.Errors.Should().BeEmpty(); @@ -122,7 +122,7 @@ public void Long_form_options_can_be_specified_using_colon_delimiter() var option = new CliOption("--hello"); var rootCommand = new CliRootCommand { option }; - var result = CliParser.Parse(rootCommand,"--hello:there"); + var result = CliParser.Parse(rootCommand, "--hello:there"); result.Errors.Should().BeEmpty(); @@ -140,39 +140,39 @@ public void Option_short_forms_can_be_bundled() var result = CliParser.Parse(command, "the-command -xyz"); result.CommandResult - .Children - .Select(o => ((OptionResult)o).Option.Name) - .Should() - .BeEquivalentTo("-x", "-y", "-z"); + .Children + .Select(o => ((OptionResult)o).Option.Name) + .Should() + .BeEquivalentTo("-x", "-y", "-z"); } /* - [Fact] - public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off() - { - // TODO: umatched tokens has been moved, fix - CliRootCommand rootCommand = new CliRootCommand() - { - new CliCommand("the-command") + [Fact] + public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off() { - new CliOption("-x"), - new CliOption("-y"), - new CliOption("-z") - } - }; - - CliConfiguration configuration = new (rootCommand) - { - EnablePosixBundling = false - }; + // TODO: umatched tokens has been moved, fix + CliRootCommand rootCommand = new CliRootCommand() + { + new CliCommand("the-command") + { + new CliOption("-x"), + new CliOption("-y"), + new CliOption("-z") + } + }; + + CliConfiguration configuration = new (rootCommand) + { + EnablePosixBundling = false + }; - var result = rootCommand.Parse("the-command -xyz", configuration); + var result = rootCommand.Parse("the-command -xyz", configuration); - result.UnmatchedTokens - .Should() - .BeEquivalentTo("-xyz"); - } + result.UnmatchedTokens + .Should() + .BeEquivalentTo("-xyz"); + } */ [Fact] @@ -181,19 +181,19 @@ public void Option_long_forms_do_not_get_unbundled() CliCommand command = new CliCommand("the-command") { - new CliOption("--xyz"), - new CliOption("-x"), - new CliOption("-y"), - new CliOption("-z") + new CliOption("--xyz"), + new CliOption("-x"), + new CliOption("-y"), + new CliOption("-z") }; var result = CliParser.Parse(command, "the-command --xyz"); result.CommandResult - .Children - .Select(o => ((OptionResult)o).Option.Name) - .Should() - .BeEquivalentTo("--xyz"); + .Children + .Select(o => ((OptionResult)o).Option.Name) + .Should() + .BeEquivalentTo("--xyz"); } [Fact] @@ -202,9 +202,9 @@ public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_v var outer = new CliCommand("outer"); outer.Options.Add(new CliOption("-a")); var inner = new CliCommand("inner") - { - new CliArgument("arg") - }; + { + new CliArgument("arg") + }; inner.Options.Add(new CliOption("-b")); inner.Options.Add(new CliOption("-c")); outer.Subcommands.Add(inner); @@ -212,10 +212,10 @@ public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_v ParseResult result = CliParser.Parse(outer, "outer inner -abc"); result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("-abc"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("-abc"); } [Fact] @@ -226,18 +226,18 @@ public void Required_option_arguments_are_not_unbundled() var optionC = new CliOption("-c"); var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-a -bc"); result.GetResult(optionA) - .Tokens - .Should() - .ContainSingle(t => t.Value == "-bc"); + .Tokens + .Should() + .ContainSingle(t => t.Value == "-bc"); } [Fact] @@ -248,11 +248,11 @@ public void Last_bundled_option_can_accept_argument_with_no_separator() var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-abcvalue"); result.GetResult(optionA).Should().NotBeNull(); @@ -272,11 +272,11 @@ public void Last_bundled_option_can_accept_argument_with_equals_separator() var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-abc=value"); result.GetResult(optionA).Should().NotBeNull(); @@ -296,11 +296,11 @@ public void Last_bundled_option_can_accept_argument_with_colon_separator() var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-abc:value"); result.GetResult(optionA).Should().NotBeNull(); @@ -320,11 +320,11 @@ public void Invalid_char_in_bundle_causes_rest_to_be_interpreted_as_value() var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-abvcalue"); result.GetResult(optionA).Should().NotBeNull(); @@ -345,10 +345,10 @@ public void Parser_root_Options_can_be_specified_multiple_times_and_their_argume var animalsOption = new CliOption("-a", "--animals"); var vegetablesOption = new CliOption("-v", "--vegetables"); var rootCommand = new CliRootCommand - { - animalsOption, - vegetablesOption - }; + { + animalsOption, + vegetablesOption + }; var result = CliParser.Parse(rootCommand, "-a cat -v carrot -a dog"); @@ -366,33 +366,34 @@ public void Parser_root_Options_can_be_specified_multiple_times_and_their_argume } /* - [Fact] - public void Options_can_be_specified_multiple_times_and_their_arguments_are_collated() - { + [Fact] + public void Options_can_be_specified_multiple_times_and_their_arguments_are_collated() + { // TODO: tests AcceptOnlyFromAmong, fix - var animalsOption = new CliOption("-a", "--animals"); - animalsOption.AcceptOnlyFromAmong("dog", "cat", "sheep"); - var vegetablesOption = new CliOption("-v", "--vegetables"); - CliCommand command = - new CliCommand("the-command") { - animalsOption, - vegetablesOption - }; - - var result = command.Parse("the-command -a cat -v carrot -a dog"); - - result.GetResult(animalsOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("cat", "dog"); - - result.GetResult(vegetablesOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("carrot"); - } + // TODO: This test does not appear to use AcceptOnlyFromAmong. Consider if test can just use normal strings + var animalsOption = new CliOption("-a", "--animals"); + animalsOption.AcceptOnlyFromAmong("dog", "cat", "sheep"); + var vegetablesOption = new CliOption("-v", "--vegetables"); + CliCommand command = + new CliCommand("the-command") { + animalsOption, + vegetablesOption + }; + + var result = command.Parse("the-command -a cat -v carrot -a dog"); + + result.GetResult(animalsOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("cat", "dog"); + + result.GetResult(vegetablesOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("carrot"); + } */ [Fact] @@ -402,68 +403,69 @@ public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_foll var vegetablesOption = new CliOption("-v", "--vegetables"); - CliCommand command = + CliCommand command = new CliCommand("the-command") { - animalsOption, - vegetablesOption, - new CliArgument("arg") + animalsOption, + vegetablesOption, + new CliArgument("arg") }; var result = CliParser.Parse(command, "the-command -a cat some-arg -v carrot"); result.GetResult(animalsOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("cat"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("cat"); result.GetResult(vegetablesOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("carrot"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("carrot"); result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("some-arg"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("some-arg"); } [Fact] public void Command_with_multiple_options_is_parsed_correctly() { var command = new CliCommand("outer") - { - new CliOption("--inner1"), - new CliOption("--inner2") - }; + { + new CliOption("--inner1"), + new CliOption("--inner2") + }; var result = CliParser.Parse(command, "outer --inner1 argument1 --inner2 argument2"); result.CommandResult - .Children - .Should() - .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner1" && - o.Tokens.Single().Value == "argument1"); + .Children + .Should() + .ContainSingle(o => + ((OptionResult)o).Option.Name == "--inner1" && + o.Tokens.Single().Value == "argument1"); result.CommandResult - .Children - .Should() - .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner2" && - o.Tokens.Single().Value == "argument2"); + .Children + .Should() + .ContainSingle(o => + ((OptionResult)o).Option.Name == "--inner2" && + o.Tokens.Single().Value == "argument2"); } - [Fact] + [Fact(Skip = "Location means these are no longer equivalent.")] + // TODO: Add comparison that ignores locations public void Relative_order_of_arguments_and_options_within_a_command_does_not_matter() { var command = new CliCommand("move") - { - new CliArgument("arg"), - new CliOption("-X") - }; + { + new CliArgument("arg"), + new CliOption("-X") + }; // option before args ParseResult result1 = CliParser.Parse( @@ -482,17 +484,17 @@ public void Relative_order_of_arguments_and_options_within_a_command_does_not_ma // all should be equivalent result1.Should() - .BeEquivalentTo( - result2, - x => x.IgnoringCyclicReferences() - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); + .BeEquivalentTo( + result2, + x => x.IgnoringCyclicReferences() + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); result1.Should() - .BeEquivalentTo( - result3, - x => x.IgnoringCyclicReferences() - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); + .BeEquivalentTo( + result3, + x => x.IgnoringCyclicReferences() + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); } [Theory] @@ -508,11 +510,11 @@ public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string c var rawSplit = CliParser.SplitCommandLine(commandLine); var command = new CliCommand("the-command") - { - new CliArgument("arg"), - new CliOption("--one"), - new CliOption("--many") - }; + { + new CliArgument("arg"), + new CliOption("--one"), + new CliOption("--many") + }; var result = CliParser.Parse(command, commandLine); @@ -520,96 +522,97 @@ public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string c } /* - [Fact] - public void An_outer_command_with_the_same_name_does_not_capture() - { - // TODO: uses Diagram, fix - var command = new CliCommand("one") - { - new CliCommand("two") + [Fact] + public void An_outer_command_with_the_same_name_does_not_capture() { - new CliCommand("three") - }, - new CliCommand("three") - }; - - ParseResult result = CliParser.Parse(command, "one two three"); - - result.Diagram().Should().Be("[ one [ two [ three ] ] ]"); - } + // TODO: uses Diagram, fix + var command = new CliCommand("one") + { + new CliCommand("two") + { + new CliCommand("three") + }, + new CliCommand("three") + }; + + ParseResult result = CliParser.Parse(command, "one two three"); + + result.Diagram().Should().Be("[ one [ two [ three ] ] ]"); + } - [Fact] - public void An_inner_command_with_the_same_name_does_not_capture() - { - // TODO: uses Diagram, fix - var command = new CliCommand("one") - { - new CliCommand("two") + [Fact] + public void An_inner_command_with_the_same_name_does_not_capture() { - new CliCommand("three") - }, - new CliCommand("three") - }; - - ParseResult result = CliParser.Parse(command, "one three"); - - result.Diagram().Should().Be("[ one [ three ] ]"); - } + // TODO: uses Diagram, fix + var command = new CliCommand("one") + { + new CliCommand("two") + { + new CliCommand("three") + }, + new CliCommand("three") + }; + + ParseResult result = CliParser.Parse(command, "one three"); + + result.Diagram().Should().Be("[ one [ three ] ]"); + } */ [Fact] public void When_nested_commands_all_accept_arguments_then_the_nearest_captures_the_arguments() { - var command = new CliCommand("outer") - { - new CliArgument("arg1"), - new CliCommand("inner") - { - new CliArgument("arg2") - } - }; + var command = new CliCommand( + "outer") + { + new CliArgument("arg1"), + new CliCommand("inner") + { + new CliArgument("arg2") + } + }; var result = CliParser.Parse(command, "outer arg1 inner arg2"); result.CommandResult - .Parent - .Tokens.Select(t => t.Value) - .Should() - .BeEquivalentTo("arg1"); + .Parent + .Tokens.Select(t => t.Value) + .Should() + .BeEquivalentTo("arg1"); result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("arg2"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("arg2"); } /* - [Fact] - public void Nested_commands_with_colliding_names_cannot_both_be_applied() - { - // TODO: uses Diagram, fix - var command = new CliCommand("outer") - { - new CliArgument("arg1"), - new CliCommand("non-unique") - { - new CliArgument("arg2") - }, - new CliCommand("inner") + [Fact] + public void Nested_commands_with_colliding_names_cannot_both_be_applied() { - new CliArgument("arg3"), - new CliCommand("non-unique") + // TODO: uses Diagram, fix + var command = new CliCommand("outer") { - new CliArgument("arg4") - } + new CliArgument("arg1"), + new CliCommand("non-unique") + { + new CliArgument("arg2") + }, + new CliCommand("inner") + { + new CliArgument("arg3"), + new CliCommand("non-unique") + { + new CliArgument("arg4") + } + } + }; + + ParseResult result = command.Parse("outer arg1 inner arg2 non-unique arg3 "); + + result.Diagram().Should().Be("[ outer [ inner [ non-unique ] ] ]"); } - }; - - ParseResult result = command.Parse("outer arg1 inner arg2 non-unique arg3 "); - - result.Diagram().Should().Be("[ outer [ inner [ non-unique ] ] ]"); - } */ [Fact] @@ -617,10 +620,10 @@ public void When_child_option_will_not_accept_arg_then_parent_can() { var option = new CliOption("-x"); var command = new CliCommand("the-command") - { - option, - new CliArgument("arg") - }; + { + option, + new CliArgument("arg") + }; var result = CliParser.Parse(command, "the-command -x the-argument"); @@ -634,9 +637,9 @@ public void When_parent_option_will_not_accept_arg_then_child_can() { var option = new CliOption("-x"); var command = new CliCommand("the-command") - { - option - }; + { + option + }; var result = CliParser.Parse(command, "the-command -x the-argument"); @@ -650,10 +653,10 @@ public void Required_arguments_on_parent_commands_do_not_create_parse_errors_whe var child = new CliCommand("child"); var parent = new CliCommand("parent") - { - new CliArgument("arg"), - child - }; + { + new CliArgument("arg"), + child + }; var result = CliParser.Parse(parent, "child"); @@ -666,13 +669,13 @@ public void Required_arguments_on_grandparent_commands_do_not_create_parse_error var grandchild = new CliCommand("grandchild"); var grandparent = new CliCommand("grandparent") - { - new CliArgument("arg"), - new CliCommand("parent") - { - grandchild - } - }; + { + new CliArgument("arg"), + new CliCommand("parent") + { + grandchild + } + }; var result = CliParser.Parse(grandparent, "parent grandchild"); @@ -683,28 +686,28 @@ public void Required_arguments_on_grandparent_commands_do_not_create_parse_error public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_at_the_end_then_it_attaches_to_the_inner_command() { var outer = new CliCommand("outer") - { - new CliCommand("inner") - { - new CliOption("-x") - }, - new CliOption("-x") - }; + { + new CliCommand("inner") + { + new CliOption("-x") + }, + new CliOption("-x") + }; ParseResult result = CliParser.Parse(outer, "outer inner -x"); result.CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .AllBeAssignableTo(); + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .AllBeAssignableTo(); result.CommandResult - .Children - .Should() - .ContainSingle(o => ((OptionResult)o).Option.Name == "-x"); + .Children + .Should() + .ContainSingle(o => ((OptionResult)o).Option.Name == "-x"); } [Fact] @@ -719,50 +722,50 @@ public void When_options_with_the_same_name_are_defined_on_parent_and_child_comm var result = CliParser.Parse(outer, "outer -x inner"); result.CommandResult - .Children - .Should() - .BeEmpty(); + .Children + .Should() + .BeEmpty(); result.CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); } /* - [Fact] - // TODO: tests unmatched tokens, needs fix - public void Arguments_only_apply_to_the_nearest_command() - { - var outer = new CliCommand("outer") - { - new CliArgument("arg1"), - new CliCommand("inner") + [Fact] + // TODO: tests unmatched tokens, needs fix + public void Arguments_only_apply_to_the_nearest_command() { - new CliArgument("arg2") + var outer = new CliCommand("outer") + { + new CliArgument("arg1"), + new CliCommand("inner") + { + new CliArgument("arg2") + } + }; + + ParseResult result = outer.Parse("outer inner arg1 arg2"); + + result.CommandResult + .Parent + .Tokens + .Should() + .BeEmpty(); + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("arg1"); + result.UnmatchedTokens + .Should() + .BeEquivalentTo("arg2"); } - }; - - ParseResult result = outer.Parse("outer inner arg1 arg2"); - - result.CommandResult - .Parent - .Tokens - .Should() - .BeEmpty(); - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("arg1"); - result.UnmatchedTokens - .Should() - .BeEquivalentTo("arg2"); - } */ [Fact] @@ -772,42 +775,39 @@ public void Options_only_apply_to_the_nearest_command() var innerOption = new CliOption("-x"); var outer = new CliCommand("outer") - { - new CliCommand("inner") - { - innerOption - }, - outerOption - }; + { + new CliCommand("inner") + { + innerOption + }, + outerOption + }; var result = CliParser.Parse(outer, "outer inner -x one -x two"); result.RootCommandResult - .GetResult(outerOption) - .Should() - .BeNull(); + .GetResult(outerOption) + .Should() + .BeNull(); } [Fact] public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_as_arguments() { var command = new CliCommand("the-command") - { - new CliCommand("complete") - { - new CliArgument("arg"), - new CliOption("--position") - } - }; - - ParseResult result = CliParser.Parse( - command, new[] { - "the-command", - "complete", - "--position", - "7", - "the-command" - }); + { + new CliCommand("complete") + { + new CliArgument("arg"), + new CliOption("--position") + } + }; + + ParseResult result = CliParser.Parse(command, new[] { "the-command", + "complete", + "--position", + "7", + "the-command" }); CommandResult completeResult = result.CommandResult; @@ -820,18 +820,18 @@ public void Absolute_unix_style_paths_are_lexed_correctly() const string commandText = @"rm ""/temp/the file.txt"""; - CliCommand command = new ("rm") - { - new CliArgument("arg") - }; + CliCommand command = new("rm") + { + new CliArgument("arg") + }; var result = CliParser.Parse(command, commandText); result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .OnlyContain(a => a == @"/temp/the file.txt"); + .Tokens + .Select(t => t.Value) + .Should() + .OnlyContain(a => a == @"/temp/the file.txt"); } [Fact] @@ -841,20 +841,20 @@ public void Absolute_Windows_style_paths_are_lexed_correctly() @"rm ""c:\temp\the file.txt\"""; CliCommand command = new("rm") - { - new CliArgument("arg") - }; + { + new CliArgument("arg") + }; ParseResult result = CliParser.Parse(command, commandText); result.CommandResult - .Tokens - .Should() - .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); + .Tokens + .Should() + .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); } -// Default values -/* + // TODO: Defaults + /* [Fact] public void Commands_can_have_default_argument_values() { @@ -864,15 +864,15 @@ public void Commands_can_have_default_argument_values() }; var command = new CliCommand("command") - { - argument - }; + { + argument + }; ParseResult result = CliParser.Parse(command, "command"); GetValue(result, argument) - .Should() - .Be("default"); + .Should() + .Be("default"); } [Fact] @@ -900,16 +900,16 @@ public void When_an_option_with_a_default_value_is_not_matched_then_the_option_r }; var command = new CliCommand("command") - { - option - }; + { + option + }; var result = CliParser.Parse(command, "command"); result.GetResult(option) - .Implicit - .Should() - .BeTrue(); + .Implicit + .Should() + .BeTrue(); } [Fact] @@ -921,16 +921,16 @@ public void When_an_option_with_a_default_value_is_not_matched_then_there_are_no }; var command = new CliCommand("command") - { - option - }; + { + option + }; var result = CliParser.Parse(command, "command"); result.GetResult(option) - .IdentifierToken - .Should() - .BeEquivalentTo(default(CliToken)); + .IdentifierToken + .Should() + .BeEquivalentTo(default(CliToken)); } [Fact] @@ -942,15 +942,15 @@ public void When_an_argument_with_a_default_value_is_not_matched_then_there_are_ }; var command = new CliCommand("command") - { - argument - }; + { + argument + }; var result = CliParser.Parse(command, "command"); result.GetResult(argument) - .Tokens - .Should() - .BeEmpty(); + .Tokens + .Should() + .BeEmpty(); } [Fact] @@ -962,107 +962,108 @@ public void Command_default_argument_value_does_not_override_parsed_value() }; var command = new CliCommand("inner") - { - argument - }; + { + argument + }; var result = CliParser.Parse(command, "the-directory"); GetValue(result, argument) - .Name - .Should() - .Be("the-directory"); + .Name + .Should() + .Be("the-directory"); } -*/ + */ + [Fact] public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_tokens() { var outer = new CliCommand("outer") - { - new CliCommand("inner") - { - new CliArgument("arg") { - Arity = ArgumentArity.OneOrMore - } - } - }; + new CliCommand("inner") + { + new CliArgument("arg") + { + Arity = ArgumentArity.OneOrMore + } + } + }; ParseResult result = CliParser.Parse(outer, "outer inner -p:RandomThing=random"); result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("-p:RandomThing=random"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("-p:RandomThing=random"); } /* - [Fact] - public void The_default_behavior_of_unmatched_tokens_resulting_in_errors_can_be_turned_off() - { + [Fact] + public void The_default_behavior_of_unmatched_tokens_resulting_in_errors_can_be_turned_off() + { // TODO: uses UnmatchedTokens, TreatUnmatchedTokensAsErrors, fix - var command = new CliCommand("the-command") - { - new CliArgument("arg") - }; - command.TreatUnmatchedTokensAsErrors = false; + var command = new CliCommand("the-command") + { + new CliArgument("arg") + }; + command.TreatUnmatchedTokensAsErrors = false; - ParseResult result = command.Parse("the-command arg1 arg2"); + ParseResult result = command.Parse("the-command arg1 arg2"); - result.Errors.Should().BeEmpty(); + result.Errors.Should().BeEmpty(); - result.UnmatchedTokens - .Should() - .BeEquivalentTo("arg2"); - } + result.UnmatchedTokens + .Should() + .BeEquivalentTo("arg2"); + } */ [Fact] public void Option_and_Command_can_have_the_same_alias() { var innerCommand = new CliCommand("inner") - { - new CliArgument("arg1") - }; + { + new CliArgument("arg1") + }; var option = new CliOption("--inner"); var outerCommand = new CliCommand("outer") - { - innerCommand, - option, - new CliArgument("arg2") - }; + { + innerCommand, + option, + new CliArgument("arg2") + }; CliParser.Parse(outerCommand, "outer inner") - .CommandResult - .Command - .Should() - .BeSameAs(innerCommand); + .CommandResult + .Command + .Should() + .BeSameAs(innerCommand); CliParser.Parse(outerCommand, "outer --inner") - .CommandResult - .Command - .Should() - .BeSameAs(outerCommand); + .CommandResult + .Command + .Should() + .BeSameAs(outerCommand); CliParser.Parse(outerCommand, "outer --inner inner") - .CommandResult - .Command - .Should() - .BeSameAs(innerCommand); + .CommandResult + .Command + .Should() + .BeSameAs(innerCommand); CliParser.Parse(outerCommand, "outer --inner inner") - .CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .Contain(o => ((OptionResult)o).Option == option); + .CommandResult + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .Contain(o => ((OptionResult)o).Option == option); } [Fact] @@ -1072,21 +1073,21 @@ public void Options_can_have_the_same_alias_differentiated_only_by_prefix() var option2 = new CliOption("--a"); var rootCommand = new CliRootCommand - { - option1, - option2 - }; + { + option1, + option2 + }; CliParser.Parse(rootCommand, "-a").CommandResult - .Children - .Select(s => ((OptionResult)s).Option) - .Should() - .BeEquivalentTo(option1); + .Children + .Select(s => ((OptionResult)s).Option) + .Should() + .BeEquivalentTo(option1); CliParser.Parse(rootCommand, "--a").CommandResult - .Children - .Select(s => ((OptionResult)s).Option) - .Should() - .BeEquivalentTo(option2); + .Children + .Select(s => ((OptionResult)s).Option) + .Should() + .BeEquivalentTo(option2); } [Theory] @@ -1116,12 +1117,12 @@ public void When_an_option_argument_is_enclosed_in_double_quotes_its_value_retai public void Trailing_option_delimiters_are_ignored() { var rootCommand = new CliRootCommand - { - new CliCommand("subcommand") - { - new CliOption("--directory") - } - }; + { + new CliCommand("subcommand") + { + new CliOption("--directory") + } + }; var args = new[] { "subcommand", "--directory:", @"c:\" }; @@ -1130,9 +1131,9 @@ public void Trailing_option_delimiters_are_ignored() result.Errors.Should().BeEmpty(); result.Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentSequenceTo(new[] { "subcommand", "--directory", @"c:\" }); + .Select(t => t.Value) + .Should() + .BeEquivalentSequenceTo(new[] { "subcommand", "--directory", @"c:\" }); } [Theory] @@ -1144,10 +1145,10 @@ public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_op var optionX = new CliOption("-x"); var command = new CliCommand("command") - { - optionX, - new CliOption("-z") - }; + { + optionX, + new CliOption("-z") + }; var result = CliParser.Parse(command, input); @@ -1162,11 +1163,11 @@ public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_bu var optionC = new CliOption("-c"); var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-a -bc"); @@ -1180,10 +1181,10 @@ public void Option_arguments_can_match_subcommands() { var optionA = new CliOption("-a"); var rootCommand = new CliRootCommand - { - new CliCommand("subcommand"), - optionA - }; + { + new CliCommand("subcommand"), + optionA + }; var result = CliParser.Parse(rootCommand, "-a subcommand"); @@ -1196,21 +1197,21 @@ public void Arguments_can_match_subcommands() { var argument = new CliArgument("arg"); var subcommand = new CliCommand("subcommand") - { - argument - }; + { + argument + }; var rootCommand = new CliRootCommand - { - subcommand - }; + { + subcommand + }; var result = CliParser.Parse(rootCommand, "subcommand one two three subcommand four"); result.CommandResult.Command.Should().BeSameAs(subcommand); GetValue(result, argument) - .Should() - .BeEquivalentSequenceTo("one", "two", "three", "subcommand", "four"); + .Should() + .BeEquivalentSequenceTo("one", "two", "three", "subcommand", "four"); } [Theory] @@ -1221,10 +1222,10 @@ public void Option_arguments_can_match_the_aliases_of_sibling_options_when_non_s var optionX = new CliOption("-x"); var command = new CliCommand("command") - { - optionX, - new CliOption("-y") - }; + { + optionX, + new CliOption("-y") + }; var result = CliParser.Parse(command, input); @@ -1238,9 +1239,9 @@ public void Single_option_arguments_that_match_option_aliases_are_parsed_correct var optionX = new CliOption("-x"); var command = new CliRootCommand - { - optionX - }; + { + optionX + }; var result = CliParser.Parse(command, "-x -x"); @@ -1262,10 +1263,10 @@ public void Boolean_options_are_not_greedy(string commandLine) var optY = new CliOption("-y"); var root = new CliRootCommand() - { - optX, - optY, - }; + { + optX, + optY, + }; var result = CliParser.Parse(root, commandLine); @@ -1282,10 +1283,10 @@ public void Multiple_option_arguments_that_match_multiple_arity_option_aliases_a var optionY = new CliOption("-y"); var command = new CliRootCommand - { - optionX, - optionY - }; + { + optionX, + optionY + }; var result = CliParser.Parse(command, "-x -x -x -y -y -x -y -y -y -x -x -y"); @@ -1300,10 +1301,10 @@ public void Bundled_option_arguments_that_match_option_aliases_are_parsed_correc var optionY = new CliOption("-y"); var command = new CliRootCommand - { - optionX, - optionY - }; + { + optionX, + optionY + }; var result = CliParser.Parse(command, "-yxx"); @@ -1317,10 +1318,10 @@ public void Argument_name_is_not_matched_as_a_token() var columnsArg = new CliArgument>("columns"); var command = new CliCommand("add") - { - nameArg, - columnsArg - }; + { + nameArg, + columnsArg + }; var result = CliParser.Parse(command, "name one two three"); @@ -1345,9 +1346,9 @@ public void Boolean_options_with_no_argument_specified_do_not_match_subsequent_a var option = new CliOption("-v"); var command = new CliCommand("command") - { - option - }; + { + option + }; var result = CliParser.Parse(command, "-v an-argument"); @@ -1355,80 +1356,80 @@ public void Boolean_options_with_no_argument_specified_do_not_match_subsequent_a } /* - [Fact] - public void When_a_command_line_has_unmatched_tokens_they_are_not_applied_to_subsequent_options() - { + [Fact] + public void When_a_command_line_has_unmatched_tokens_they_are_not_applied_to_subsequent_options() + { // TODO: uses TreatUnmatchedTokensAsErrors, fix - var command = new CliCommand("command") - { - TreatUnmatchedTokensAsErrors = false - }; - var optionX = new CliOption("-x"); - command.Options.Add(optionX); - var optionY = new CliOption("-y"); - command.Options.Add(optionY); - - var result = command.Parse("-x 23 unmatched-token -y 42"); - - GetValue(result, optionX).Should().Be("23"); - GetValue(result, optionY).Should().Be("42"); - result.UnmatchedTokens.Should().BeEquivalentTo("unmatched-token"); - } + var command = new CliCommand("command") + { + TreatUnmatchedTokensAsErrors = false + }; + var optionX = new CliOption("-x"); + command.Options.Add(optionX); + var optionY = new CliOption("-y"); + command.Options.Add(optionY); + + var result = command.Parse("-x 23 unmatched-token -y 42"); + + GetValue(result, optionX).Should().Be("23"); + GetValue(result, optionY).Should().Be("42"); + result.UnmatchedTokens.Should().BeEquivalentTo("unmatched-token"); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void When_a_command_line_has_unmatched_tokens_the_parse_result_action_should_depend_on_parsed_command_TreatUnmatchedTokensAsErrors(bool treatUnmatchedTokensAsErrors) - { - // TODO: uses TreatUnmatchedTokensAsErrors, fix - CliRootCommand rootCommand = new(); - CliCommand subcommand = new("vstest") - { - new CliOption("--Platform"), - new CliOption("--Framework"), - new CliOption("--logger") - }; - subcommand.TreatUnmatchedTokensAsErrors = treatUnmatchedTokensAsErrors; - rootCommand.Subcommands.Add(subcommand); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void When_a_command_line_has_unmatched_tokens_the_parse_result_action_should_depend_on_parsed_command_TreatUnmatchedTokensAsErrors(bool treatUnmatchedTokensAsErrors) + { + // TODO: uses TreatUnmatchedTokensAsErrors, fix + CliRootCommand rootCommand = new(); + CliCommand subcommand = new("vstest") + { + new CliOption("--Platform"), + new CliOption("--Framework"), + new CliOption("--logger") + }; + subcommand.TreatUnmatchedTokensAsErrors = treatUnmatchedTokensAsErrors; + rootCommand.Subcommands.Add(subcommand); - var result = rootCommand.Parse("vstest test1.dll test2.dll"); + var result = rootCommand.Parse("vstest test1.dll test2.dll"); - result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); + result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); - if (treatUnmatchedTokensAsErrors) - { - result.Errors.Should().NotBeEmpty(); - result.Action.Should().NotBeSameAs(result.CommandResult.Command.Action); - } - else - { - result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); - } - } + if (treatUnmatchedTokensAsErrors) + { + result.Errors.Should().NotBeEmpty(); + result.Action.Should().NotBeSameAs(result.CommandResult.Command.Action); + } + else + { + result.Errors.Should().BeEmpty(); + result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + } + } - [Fact] - public void RootCommand_TreatUnmatchedTokensAsErrors_set_to_false_has_precedence_over_subcommands() - { - // TODO: uses TreatUnmatchedTokensAsErrors, fix - CliRootCommand rootCommand = new(); - rootCommand.TreatUnmatchedTokensAsErrors = false; - CliCommand subcommand = new("vstest") - { - new CliOption("--Platform"), - new CliOption("--Framework"), - new CliOption("--logger") - }; - subcommand.TreatUnmatchedTokensAsErrors = true; // the default, set to true to make it explicit - rootCommand.Subcommands.Add(subcommand); + [Fact] + public void RootCommand_TreatUnmatchedTokensAsErrors_set_to_false_has_precedence_over_subcommands() + { + // TODO: uses TreatUnmatchedTokensAsErrors, fix + CliRootCommand rootCommand = new(); + rootCommand.TreatUnmatchedTokensAsErrors = false; + CliCommand subcommand = new("vstest") + { + new CliOption("--Platform"), + new CliOption("--Framework"), + new CliOption("--logger") + }; + subcommand.TreatUnmatchedTokensAsErrors = true; // the default, set to true to make it explicit + rootCommand.Subcommands.Add(subcommand); - var result = rootCommand.Parse("vstest test1.dll test2.dll"); + var result = rootCommand.Parse("vstest test1.dll test2.dll"); - result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); + result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); - result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); - } + result.Errors.Should().BeEmpty(); + result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + } */ [Fact] @@ -1447,18 +1448,18 @@ public void Command_argument_arity_can_be_a_fixed_value_greater_than_1() Arity = new ArgumentArity(3, 3) }; var command = new CliCommand("the-command") - { - argument - }; + { + argument + }; CliParser.Parse(command, "1 2 3") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument,dummyLocation), - new CliToken("2", CliTokenType.Argument, argument, dummyLocation), - new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument, dummyLocation), + new CliToken("2", CliTokenType.Argument, argument, dummyLocation), + new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); } [Fact] @@ -1469,90 +1470,87 @@ public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_tha Arity = new ArgumentArity(3, 5) }; var command = new CliCommand("the-command") - { - argument - }; + { + argument + }; CliParser.Parse(command, "1 2 3") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument, dummyLocation), - new CliToken("2", CliTokenType.Argument, argument, dummyLocation), - new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument, dummyLocation), + new CliToken("2", CliTokenType.Argument, argument, dummyLocation), + new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); CliParser.Parse(command, "1 2 3 4 5") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument, dummyLocation), - new CliToken("2", CliTokenType.Argument, argument, dummyLocation), - new CliToken("3", CliTokenType.Argument, argument, dummyLocation), - new CliToken("4", CliTokenType.Argument, argument, dummyLocation), - new CliToken("5", CliTokenType.Argument, argument, dummyLocation)); + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument, dummyLocation), + new CliToken("2", CliTokenType.Argument, argument, dummyLocation), + new CliToken("3", CliTokenType.Argument, argument, dummyLocation), + new CliToken("4", CliTokenType.Argument, argument, dummyLocation), + new CliToken("5", CliTokenType.Argument, argument, dummyLocation)); } -// TODO: Validation? -/* [Fact] public void When_command_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() { var command = new CliCommand("the-command") - { - new CliArgument("arg") - { - Arity = new ArgumentArity(2, 3) - } - }; + { + new CliArgument("arg") + { + Arity = new ArgumentArity(2, 3) + } + }; var result = CliParser.Parse(command, "1"); result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(command.Arguments[0]))); + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(command.Arguments[0]))); } [Fact] public void When_command_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() { var command = new CliCommand("the-command") - { - new CliArgument("arg") - { - Arity = new ArgumentArity(2, 3) - } - }; + { + new CliArgument("arg") + { + Arity = new ArgumentArity(2, 3) + } + }; ParseResult parseResult = CliParser.Parse(command, "1 2 3 4"); parseResult - .Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); + .Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); } -*/ [Fact] public void Option_argument_arity_can_be_a_fixed_value_greater_than_1() { - var option = new CliOption("-x") { Arity = new ArgumentArity(3, 3)}; + var option = new CliOption("-x") { Arity = new ArgumentArity(3, 3) }; var command = new CliCommand("the-command") - { - option - }; + { + option + }; CliParser.Parse(command, "-x 1 -x 2 -x 3") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default, dummyLocation), - new CliToken("2", CliTokenType.Argument, default, dummyLocation), - new CliToken("3", CliTokenType.Argument, default, dummyLocation)); + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default, dummyLocation), + new CliToken("2", CliTokenType.Argument, default, dummyLocation), + new CliToken("3", CliTokenType.Argument, default, dummyLocation)); } [Fact] @@ -1561,32 +1559,30 @@ public void Option_argument_arity_can_be_a_range_with_a_lower_bound_greater_than var option = new CliOption("-x") { Arity = new ArgumentArity(3, 5) }; var command = new CliCommand("the-command") - { - option - }; + { + option + }; CliParser.Parse(command, "-x 1 -x 2 -x 3") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default, dummyLocation), - new CliToken("2", CliTokenType.Argument, default, dummyLocation), - new CliToken("3", CliTokenType.Argument, default, dummyLocation)); + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default, dummyLocation), + new CliToken("2", CliTokenType.Argument, default, dummyLocation), + new CliToken("3", CliTokenType.Argument, default, dummyLocation)); CliParser.Parse(command, "-x 1 -x 2 -x 3 -x 4 -x 5") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default, dummyLocation), - new CliToken("2", CliTokenType.Argument, default, dummyLocation), - new CliToken("3", CliTokenType.Argument, default, dummyLocation), - new CliToken("4", CliTokenType.Argument, default, dummyLocation), - new CliToken("5", CliTokenType.Argument, default, dummyLocation)); + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default, dummyLocation), + new CliToken("2", CliTokenType.Argument, default, dummyLocation), + new CliToken("3", CliTokenType.Argument, default, dummyLocation), + new CliToken("4", CliTokenType.Argument, default, dummyLocation), + new CliToken("5", CliTokenType.Argument, default, dummyLocation)); } -// TODO: Validation? -/* [Fact] public void When_option_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() { @@ -1596,33 +1592,32 @@ public void When_option_arguments_are_fewer_than_minimum_arity_then_an_error_is_ }; var command = new CliCommand("the-command") - { - option - }; + { + option + }; var result = CliParser.Parse(command, "-x 1"); result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(option))); + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(option))); } [Fact] public void When_option_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() { var command = new CliCommand("the-command") - { - new CliOption("-x") { Arity = new ArgumentArity(2, 3)} - }; + { + new CliOption("-x") { Arity = new ArgumentArity(2, 3)} + }; CliParser.Parse(command, "-x 1 2 3 4") - .Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); + .Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); } -*/ [Fact] public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_option() @@ -1632,38 +1627,38 @@ public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_opti var result = CliParser.Parse(rootCommand, "jdbc url \"jdbc:sqlserver://10.0.0.2;databaseName=main\""); result.Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("url", - "jdbc:sqlserver://10.0.0.2;databaseName=main"); + .Select(t => t.Value) + .Should() + .BeEquivalentTo("url", + "jdbc:sqlserver://10.0.0.2;databaseName=main"); } /* - [Fact] - public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() - { - // TODO: uses GetCompletions, fix - // Tests bug identified in https://github.com/dotnet/command-line-api/issues/997 + [Fact] + public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() + { + // TODO: uses GetCompletions, fix + // Tests bug identified in https://github.com/dotnet/command-line-api/issues/997 - var argument1 = new CliArgument("arg1"); + var argument1 = new CliArgument("arg1"); - var argument2 = new CliArgument("arg2"); + var argument2 = new CliArgument("arg2"); - var command = new CliCommand("subcommand") - { - argument1, - argument2 - }; + var command = new CliCommand("subcommand") + { + argument1, + argument2 + }; - var rootCommand = new CliRootCommand - { - command - }; + var rootCommand = new CliRootCommand + { + command + }; - var parseResult = rootCommand.Parse("subcommand arg1 arg2"); + var parseResult = rootCommand.Parse("subcommand arg1 arg2"); - Action act = () => parseResult.GetCompletions(); - act.Should().NotThrow(); - } + Action act = () => parseResult.GetCompletions(); + act.Should().NotThrow(); + } */ [Theory] // https://github.com/dotnet/command-line-api/issues/1551, https://github.com/dotnet/command-line-api/issues/1533 @@ -1678,13 +1673,268 @@ public void Parsed_value_of_empty_string_arg_is_an_empty_string(string arg1, str }; var rootCommand = new CliRootCommand - { - option - }; + { + option + }; var result = CliParser.Parse(rootCommand, new[] { arg1, arg2 }); GetValue(result, option).Should().BeEmpty(); } + + // TODO: Tests below are from Powderhouse. Consider whether this the right location considering how large the file is + [Fact] + public void CommandResult_contains_argument_ValueResults() + { + var argument1 = new CliArgument("arg1"); + var argument2 = new CliArgument("arg2"); + + var command = new CliCommand("subcommand") + { + argument1, + argument2 + }; + + var rootCommand = new CliRootCommand + { + command + }; + + var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); + + + var commandResult = parseResult.CommandResult; + commandResult.ValueResults.Should().HaveCount(2); + var result1 = commandResult.ValueResults[0]; + result1.GetValue().Should().Be("Kirk"); + var result2 = commandResult.ValueResults[1]; + result2.GetValue().Should().Be("Spock"); + } + + [Fact] + public void CommandResult_contains_option_ValueResults() + { + var argument1 = new CliOption("--opt1"); + var argument2 = new CliOption("--opt2"); + + var command = new CliCommand("subcommand") + { + argument1, + argument2 + }; + + var rootCommand = new CliRootCommand + { + command + }; + + var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); + + + var commandResult = parseResult.CommandResult; + commandResult.ValueResults.Should().HaveCount(2); + var result1 = commandResult.ValueResults[0]; + result1.GetValue().Should().Be("Kirk"); + var result2 = commandResult.ValueResults[1]; + result2.GetValue().Should().Be("Spock"); + } + + [Fact] + public void Location_in_ValueResult_correct_for_arguments() + { + var argument1 = new CliArgument("arg1"); + var argument2 = new CliArgument("arg2"); + + var command = new CliCommand("subcommand") + { + argument1, + argument2 + }; + + var rootCommand = new CliRootCommand + { + command + }; + var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedLocation1 = new Location("Kirk", Location.User, 1, expectedOuterLocation); + var expectedLocation2 = new Location("Spock", Location.User, 2, expectedOuterLocation); + + var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); + + + var commandResult = parseResult.CommandResult; + var result1 = commandResult.ValueResults[0]; + var result2 = commandResult.ValueResults[1]; + result1.Locations.Single().Should().Be(expectedLocation1); + result2.Locations.Single().Should().Be(expectedLocation2); + } + + [Fact] + public void Location_in_ValueResult_correct_for_options() + { + var option1 = new CliOption("--opt1"); + var option2 = new CliOption("--opt2"); + + var command = new CliCommand("subcommand") + { + option1, + option2 + }; + + var rootCommand = new CliRootCommand + { + command + }; + var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedLocation1 = new Location("Kirk", Location.User, 3, expectedOuterLocation); + var expectedLocation2 = new Location("Spock", Location.User, 5, expectedOuterLocation); + + var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); + + var commandResult = parseResult.CommandResult; + var result1 = commandResult.ValueResults[0]; + var result2 = commandResult.ValueResults[1]; + result1.Locations.Single().Should().Be(expectedLocation1); + result2.Locations.Single().Should().Be(expectedLocation2); + } + + [Fact] + public void Location_offsets_in_ValueResult_correct_for_arguments() + { + var argument1 = new CliArgument("arg1"); + + var command = new CliCommand("subcommand") + { + argument1, + }; + + var rootCommand = new CliRootCommand + { + command + }; + var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedLocation1 = new Location("Kirk", Location.User, 1, expectedOuterLocation); + var expectedLocation2 = new Location("Spock", Location.User, 2, expectedOuterLocation); + + var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); + + var commandResult = parseResult.CommandResult; + var result1 = commandResult.ValueResults.Single(); + result1.Locations.First().Should().Be(expectedLocation1); + result1.Locations.Skip(1).Single().Should().Be(expectedLocation2); + } + + [Fact] + public void Location_offsets_in_ValueResult_correct_for_options() + { + var option1 = new CliOption("--opt1"); + + var command = new CliCommand("subcommand") + { + option1, + }; + + var rootCommand = new CliRootCommand + { + command + }; + var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedLocation1 = new Location("Kirk", Location.User, 3, expectedOuterLocation); + var expectedLocation2 = new Location("Spock", Location.User, 5, expectedOuterLocation); + + var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt1 Spock"); + + var commandResult = parseResult.CommandResult; + var result1 = commandResult.ValueResults.Single(); + result1.Locations.First().Should().Be(expectedLocation1); + result1.Locations.Skip(1).Single().Should().Be(expectedLocation2); + } + + [Fact] + public void Location_offset_correct_when_colon_or_equal_used() + { + var option1 = new CliOption("--opt1"); + var option2 = new CliOption("--opt11"); + + var command = new CliCommand("subcommand") + { + option1, + option2 + }; + + var rootCommand = new CliRootCommand + { + command + }; + var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedLocation1 = new Location("Kirk", Location.User, 2, expectedOuterLocation, 7); + var expectedLocation2 = new Location("Spock", Location.User, 3, expectedOuterLocation, 8); + + var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1:Kirk --opt11=Spock"); + + var commandResult = parseResult.CommandResult; + var result1 = commandResult.ValueResults[0]; + var result2 = commandResult.ValueResults[1]; + result1.Locations.Single().Should().Be(expectedLocation1); + result2.Locations.Single().Should().Be(expectedLocation2); + } + + [Fact] + public void ParseResult_contains_argument_ValueResults() + { + var argument1 = new CliArgument("arg1"); + var argument2 = new CliArgument("arg2"); + + var command = new CliCommand("subcommand") + { + argument1, + argument2 + }; + + var rootCommand = new CliRootCommand + { + command + }; + + var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); + + + var result1 = parseResult.GetValueResult(argument1); + var result2 = parseResult.GetValueResult(argument2); + result1.GetValue().Should().Be("Kirk"); + result2.GetValue().Should().Be("Spock"); + } + + [Fact] + public void ParseResult_contains_option_ValueResults() + { + var option1 = new CliOption("--opt1"); + var option2 = new CliOption("--opt2"); + + var command = new CliCommand("subcommand") + { + option1, + option2 + }; + + var rootCommand = new CliRootCommand + { + command + }; + + var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); + + + var result1 = parseResult.GetValueResult(option1); + var result2 = parseResult.GetValueResult(option2); + result1.GetValue().Should().Be("Kirk"); + result2.GetValue().Should().Be("Spock"); + } + + [Fact] + public void Value_result_returned_in_simple_case() + { + + } } } diff --git a/src/System.CommandLine.Tests/TokenizerTests.cs b/src/System.CommandLine.Tests/TokenizerTests.cs index 1b810e5e43..854ec3125a 100644 --- a/src/System.CommandLine.Tests/TokenizerTests.cs +++ b/src/System.CommandLine.Tests/TokenizerTests.cs @@ -23,6 +23,7 @@ public void The_tokenizer_can_handle_single_option() List tokens = null; List errors = null; Tokenizer.Tokenize(args, command, new CliConfiguration(command), true, out tokens, out errors); + Tokenizer.Tokenize(args, command, new CliConfiguration(command), true, out tokens, out errors); tokens .Skip(1) @@ -34,7 +35,7 @@ public void The_tokenizer_can_handle_single_option() } [Fact] - public void Location_stack_is_correct() + public void Location_stack_ToString_is_correct() { var option = new CliOption("--hello"); var command = new CliRootCommand { option }; @@ -42,8 +43,6 @@ public void Location_stack_is_correct() List tokens = null; List errors = null; - int rootCommandNameLength = CliExecutable.ExecutableName.Length; - Tokenizer.Tokenize(args, command, new CliConfiguration(command), @@ -58,8 +57,8 @@ public void Location_stack_is_correct() errors.Should().BeNull(); tokens.Count.Should().Be(3); locations.Count.Should().Be(2); - locations[0].Should().Be($"User [-1, {rootCommandNameLength}, 0]; User [0, 7, 0]"); - locations[1].Should().Be($"User [-1, {rootCommandNameLength}, 0]; User [1, 5, 0]"); + locations[0].Should().Be("testhost from User[-1, 8, 0]; --hello from User[0, 7, 0]"); + locations[1].Should().Be("testhost from User[-1, 8, 0]; world from User[1, 5, 0]"); } [Fact] diff --git a/src/System.CommandLine/ArgumentArity.cs b/src/System.CommandLine/ArgumentArity.cs index 72acdbdbec..efebad3713 100644 --- a/src/System.CommandLine/ArgumentArity.cs +++ b/src/System.CommandLine/ArgumentArity.cs @@ -58,11 +58,11 @@ public ArgumentArity(int minimumNumberOfValues, int maximumNumberOfValues) /// public int MaximumNumberOfValues { get; } - internal bool IsNonDefault { get; } + internal bool IsNonDefault { get; } /// - public bool Equals(ArgumentArity other) => - other.MaximumNumberOfValues == MaximumNumberOfValues && + public bool Equals(ArgumentArity other) => + other.MaximumNumberOfValues == MaximumNumberOfValues && other.MinimumNumberOfValues == MinimumNumberOfValues && other.IsNonDefault == IsNonDefault; diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 9c8e96cea6..32a51061bc 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -14,6 +14,8 @@ namespace System.CommandLine /// public sealed class ParseResult { + private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); + private readonly CommandResult _rootCommandResult; // TODO: unmatched tokens, invocation, completion /* @@ -28,6 +30,7 @@ internal ParseResult( // TODO: determine how rootCommandResult and commandResult differ CommandResult rootCommandResult, CommandResult commandResult, + Dictionary valueResults, List tokens, // TODO: unmatched tokens // List? unmatchedTokens, @@ -44,6 +47,7 @@ internal ParseResult( Configuration = configuration; _rootCommandResult = rootCommandResult; CommandResult = commandResult; + this.valueResultDictionary = valueResults; // TODO: invocation /* _action = action; @@ -98,11 +102,13 @@ internal ParseResult( public IReadOnlyList Errors { get; } // TODO: don't expose tokens + // TODO: This appears to be set, but only read during testing. Consider removing. /// /// Gets the tokens identified while parsing command line input. /// internal IReadOnlyList Tokens { get; } + // TODO: This appears to be set, but never used. Consider removing. /// /// Holds the value of a complete command line input prior to splitting and tokenization, when provided. /// @@ -159,6 +165,9 @@ CommandLineText is null public override string ToString() => ParseDiagramAction.Diagram(this).ToString(); */ + public ValueResult? GetValueResult(CliSymbol symbol) + => valueResultDictionary[symbol]; + /// /// Gets the result, if any, for the specified argument. /// diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/ArgumentResult.cs index 49fedd4667..5ec24b3ef5 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/ArgumentResult.cs @@ -22,6 +22,24 @@ internal ArgumentResult( Argument = argument ?? throw new ArgumentNullException(nameof(argument)); } + private ValueResult? _valueResult; + public ValueResult ValueResult + { + get + { + if (_valueResult is null) + { + // This is not lazy on the assumption that almost everything the user enters will be used, and ArgumentResult is no longer used for defaults + // TODO: Make sure errors are added + var conversionValue = GetArgumentConversionResult().Value; + var locations = Tokens.Select(token => token.Location).ToArray(); + //TODO: Remove this wrapper later + _valueResult = new ValueResult(Argument, conversionValue, locations, ValueResultExtensions.GetValueResultOutcome(GetArgumentConversionResult()?.Result)); // null is temporary here + } + return _valueResult; + } + } + /// /// The argument to which the result applies. /// @@ -29,6 +47,7 @@ internal ArgumentResult( internal bool ArgumentLimitReached => Argument.Arity.MaximumNumberOfValues == (_tokens?.Count ?? 0); + internal ArgumentConversionResult GetArgumentConversionResult() => _conversionResult ??= ValidateAndConvert(useValidators: true); @@ -133,26 +152,29 @@ private ArgumentConversionResult ValidateAndConvert(bool useValidators) { return ReportErrorIfNeeded(arityFailure); } -// TODO: validators -/* - // 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) - { - for (var i = 0; i < Argument.Validators.Count; i++) - { - Argument.Validators[i](this); - } - - // validator provided by the user might report an error, which sets _conversionResult - if (_conversionResult is not null) - { - return _conversionResult; - } - } -*/ + // TODO: validators + /* + // 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) + { + for (var i = 0; i < Argument.Validators.Count; i++) + { + Argument.Validators[i](this); + } + + // validator provided by the user might report an error, which sets _conversionResult + if (_conversionResult is not null) + { + return _conversionResult; + } + } + */ + + // TODO: defaults + /* if (Parent!.UseDefaultValueFor(this)) { var defaultValue = Argument.GetDefaultValue(this); @@ -160,6 +182,7 @@ private ArgumentConversionResult ValidateAndConvert(bool useValidators) // 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) { @@ -193,7 +216,7 @@ private ArgumentConversionResult ValidateAndConvert(bool useValidators) ArgumentConversionResult.ArgumentConversionCannotParse( this, Argument.ValueType, - Tokens.Count > 0 + Tokens.Count > 0 ? Tokens[0].Value : "")); @@ -211,7 +234,7 @@ ArgumentConversionResult ReportErrorIfNeeded(ArgumentConversionResult result) /// /// Since Option.Argument is an internal implementation detail, this ArgumentResult applies to the OptionResult in public API if the parent is an OptionResult. /// - private SymbolResult AppliesToPublicSymbolResult => + private SymbolResult AppliesToPublicSymbolResult => Parent is OptionResult optionResult ? optionResult : this; } } diff --git a/src/System.CommandLine/Parsing/CliToken.cs b/src/System.CommandLine/Parsing/CliToken.cs index d692285332..ce7e1aeb65 100644 --- a/src/System.CommandLine/Parsing/CliToken.cs +++ b/src/System.CommandLine/Parsing/CliToken.cs @@ -18,7 +18,6 @@ public static CliToken CreateFromOtherToken(CliToken otherToken, string? arg, Lo /// The string value of the token. /// The type of the token. /// The symbol represented by the token - /// The location of the token /* public CliToken(string? value, CliTokenType type, CliSymbol symbol) { diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CommandResult.cs index de5fb1f507..a1802f7c1c 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CommandResult.cs @@ -38,6 +38,16 @@ internal CommandResult( /// public IEnumerable Children => SymbolResultTree.GetChildren(this); + public IReadOnlyList ValueResults => Children.Select(GetValueResult).OfType().ToList(); + + private ValueResult? GetValueResult(SymbolResult symbolResult) + => symbolResult switch + { + ArgumentResult argumentResult => argumentResult.ValueResult, + OptionResult optionResult => optionResult.ValueResult, + _ => null! + }; + /// public override string ToString() => $"{nameof(CommandResult)}: {IdentifierToken.Value} {string.Join(" ", Tokens.Select(t => t.Value))}"; @@ -75,6 +85,7 @@ internal void Validate(bool completeValidation) */ } + // TODO: Validation if (Command.HasOptions) { ValidateOptions(completeValidation); @@ -155,11 +166,15 @@ private void ValidateOptions(bool completeValidation) } */ + // TODO: Ensure all argument conversions are run for entered values + /* _ = argumentResult.GetArgumentConversionResult(); + */ } } - private void ValidateArguments(bool completeValidation) + // TODO: Validation + private void ValidateArguments(bool completeValidation) { var arguments = Command.Arguments; for (var i = 0; i < arguments.Count; i++) diff --git a/src/System.CommandLine/Parsing/CommandValueResult.cs b/src/System.CommandLine/Parsing/CommandValueResult.cs new file mode 100644 index 0000000000..0de61e9953 --- /dev/null +++ b/src/System.CommandLine/Parsing/CommandValueResult.cs @@ -0,0 +1,17 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace System.CommandLine.Parsing; + +public class CommandValueResult +{ + IEnumerable ValueResults { get; } = new List(); + + +} diff --git a/src/System.CommandLine/Parsing/Location.cs b/src/System.CommandLine/Parsing/Location.cs index bfaa877cda..18d97ce681 100644 --- a/src/System.CommandLine/Parsing/Location.cs +++ b/src/System.CommandLine/Parsing/Location.cs @@ -50,7 +50,7 @@ public bool IsImplicit => Source == Implicit; public override string ToString() - => $"{(OuterLocation is null ? "" : OuterLocation.ToString() + "; ")}{Source} [{Start}, {Length}, {Offset}]"; + => $"{(OuterLocation is null ? "" : OuterLocation.ToString() + "; ")}{Text} from {Source}[{Start}, {Length}, {Offset}]"; } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/OptionResult.cs index d19ebff5b3..24b7e765ef 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/OptionResult.cs @@ -25,6 +25,24 @@ internal OptionResult( IdentifierToken = token; } + private ValueResult? _valueResult; + public ValueResult ValueResult + { + get + { + if (_valueResult is null) + { + // This is not lazy on the assumption that almost everything the user enters will be used, and ArgumentResult is no longer used for defaults + // TODO: Make sure errors are added + var conversionValue = ArgumentConversionResult.Value; + var locations = Tokens.Select(token => token.Location).ToArray(); + //TODO: Remove this wrapper later + _valueResult = new ValueResult(Option, conversionValue, locations, ValueResultExtensions.GetValueResultOutcome(ArgumentConversionResult?.Result)); // null is temporary here + } + return _valueResult; + } + } + /// /// The option to which the result applies. /// diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 56d6373ff0..48e0852253 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -85,9 +85,11 @@ internal ParseResult Parse() _configuration, _rootCommandResult, _innermostCommandResult, + _rootCommandResult.SymbolResultTree.GetValueResultDictionary(), _tokens, // TODO: unmatched tokens // _symbolResultTree.UnmatchedTokens, + _symbolResultTree.Errors, _rawInput // TODO: invocation diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 9927aa4dd9..5c38e21479 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -187,7 +187,7 @@ internal static void Tokenize( return errors; } - + static bool TryGetSymbolAndTokenType(Dictionary validTokens, string arg, [NotNullWhen(true)] out CliSymbol? symbol, diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index d235611d79..2f15c15a7e 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.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.Linq; namespace System.CommandLine.Parsing { @@ -9,13 +10,17 @@ internal sealed class SymbolResultTree : Dictionary { private readonly CliCommand _rootCommand; internal List? Errors; -// TODO: unmatched tokens -/* - internal List? UnmatchedTokens; -*/ + // TODO: unmatched tokens + /* + internal List? UnmatchedTokens; + */ + + // TODO: Looks like this is a SymboNode/linked list because a symbol may appear multiple + // places in the tree and multiple symbols will have the same short name. The question is + // whether creating the multiple node instances is faster than just using lists. Could well be. private Dictionary? _symbolsByName; internal SymbolResultTree( - CliCommand rootCommand, + CliCommand rootCommand, List? tokenizeErrors) { _rootCommand = rootCommand; @@ -41,13 +46,15 @@ internal SymbolResultTree( internal OptionResult? GetResult(CliOption option) => TryGetValue(option, out SymbolResult? result) ? (OptionResult)result : default; -//TODO: directives -/* - internal DirectiveResult? GetResult(CliDirective directive) - => TryGetValue(directive, out SymbolResult? result) ? (DirectiveResult)result : default; -*/ + //TODO: directives + /* + internal DirectiveResult? GetResult(CliDirective directive) + => TryGetValue(directive, out SymbolResult? result) ? (DirectiveResult)result : default; + */ + // TODO: Determine how this is used. It appears to be O^n in the size of the tree and so if it is called multiple times, we should reconsider to avoid O^(N*M) internal IEnumerable GetChildren(SymbolResult parent) { + // Argument can't have children if (parent is not ArgumentResult) { foreach (KeyValuePair pair in this) @@ -60,35 +67,56 @@ internal IEnumerable GetChildren(SymbolResult parent) } } + internal Dictionary GetValueResultDictionary() + { + var dict = new Dictionary(); + foreach (KeyValuePair pair in this) + { + var result = pair.Value; + if (result is OptionResult optionResult) + { + dict.Add(pair.Key, optionResult.ValueResult); + continue; + } + if (result is ArgumentResult argumentResult) + { + dict.Add(pair.Key, argumentResult.ValueResult); + continue; + } + } + return dict; + } + internal void AddError(ParseError parseError) => (Errors ??= new()).Add(parseError); internal void InsertFirstError(ParseError parseError) => (Errors ??= new()).Insert(0, parseError); internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, CommandResult rootCommandResult) { -/* -// TODO: unmatched tokens - (UnmatchedTokens ??= new()).Add(token); - - if (commandResult.Command.TreatUnmatchedTokensAsErrors) - { - if (commandResult != rootCommandResult && !rootCommandResult.Command.TreatUnmatchedTokensAsErrors) - { - return; - } - -*/ - AddError(new ParseError(LocalizationResources.UnrecognizedCommandOrArgument(token.Value), commandResult)); -// } + /* + // TODO: unmatched tokens + (UnmatchedTokens ??= new()).Add(token); + + if (commandResult.Command.TreatUnmatchedTokensAsErrors) + { + if (commandResult != rootCommandResult && !rootCommandResult.Command.TreatUnmatchedTokensAsErrors) + { + return; + } + + */ + AddError(new ParseError(LocalizationResources.UnrecognizedCommandOrArgument(token.Value), commandResult)); + // } } public SymbolResult? GetResult(string name) { if (_symbolsByName is null) { - _symbolsByName = new(); + _symbolsByName = new(); + // TODO: See if we can avoid populating the entire tree and just populate the portion/cone we need PopulateSymbolsByName(_rootCommand); } - + if (!_symbolsByName.TryGetValue(name, out SymbolNode? node)) { throw new ArgumentException($"No symbol result found with name \"{name}\"."); @@ -107,11 +135,12 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com return null; } -// TODO: symbolsbyname - this is inefficient -// results for some values may not be queried at all, dependent on other options -// so we could avoid using their value factories and adding them to the dictionary -// could we sort by name allowing us to do a binary search instead of allocating a dictionary? -// could we add codepaths that query for specific kinds of symbols so they don't have to search all symbols? + // TODO: symbolsbyname - this is inefficient + // results for some values may not be queried at all, dependent on other options + // so we could avoid using their value factories and adding them to the dictionary + // could we sort by name allowing us to do a binary search instead of allocating a dictionary? + // could we add codepaths that query for specific kinds of symbols so they don't have to search all symbols? + // Additional Note: Couldn't commands know their children, and thus this involves querying the active command, and possibly the parents private void PopulateSymbolsByName(CliCommand command) { if (command.HasArguments) @@ -140,6 +169,7 @@ private void PopulateSymbolsByName(CliCommand command) } } + // TODO: Explore removing closure here void AddToSymbolsByName(CliSymbol symbol) { if (_symbolsByName!.TryGetValue(symbol.Name, out var node)) diff --git a/src/System.CommandLine/Parsing/ValueResult.cs b/src/System.CommandLine/Parsing/ValueResult.cs new file mode 100644 index 0000000000..8899576224 --- /dev/null +++ b/src/System.CommandLine/Parsing/ValueResult.cs @@ -0,0 +1,61 @@ +// 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; + +public class ValueResult +{ + internal ValueResult( + CliSymbol valueSymbol, + object? value, + IEnumerable locations, + ValueResultOutcome outcome, + // TODO: Error should be an Enumerable and perhaps should not be here at all, only on ParseResult + string? error = null) + { + ValueSymbol = valueSymbol; + Value = value; + Locations = locations; + Outcome = outcome; + Error = error; + } + + public CliSymbol ValueSymbol { get; } + internal object? Value { get; } + + public T? GetValue() + => (T?)Value; + + // This needs to be a collection because collection types have multiple tokens and they will not be simple offsets when response files are used + // TODO: Consider more efficient ways to do this in the case where there is a single location + public IEnumerable Locations { get; } + + public ValueResultOutcome Outcome { get; } + + public string? Error { get; } + + public override string ToString() + => $"{nameof(ValueResult)} ({FormatOutcomeMessage()}) {ValueSymbol?.Name}"; + + // TODO: This definitely feels like the wrong place for this, (Some completion stuff was stripped out. This was a private method in ArgumentConversionResult + private string FormatOutcomeMessage() + => ValueSymbol switch + { + CliOption option + => LocalizationResources.ArgumentConversionCannotParseForOption(Value?.ToString() ?? "", option.Name, ValueSymbolType), + CliCommand command + => LocalizationResources.ArgumentConversionCannotParseForCommand(Value?.ToString() ?? "", command.Name, ValueSymbolType), + //TODO + _ => throw new NotImplementedException() + }; + + private Type ValueSymbolType + => ValueSymbol switch + { + CliArgument argument => argument.ValueType, + CliOption option => option.Argument.ValueType, + _ => throw new NotImplementedException() + }; +} diff --git a/src/System.CommandLine/Parsing/ValueResultExtensions.cs b/src/System.CommandLine/Parsing/ValueResultExtensions.cs new file mode 100644 index 0000000000..2fa36d54b9 --- /dev/null +++ b/src/System.CommandLine/Parsing/ValueResultExtensions.cs @@ -0,0 +1,17 @@ +// 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 ValueResultExtensions +{ + internal static ValueResultOutcome GetValueResultOutcome(ArgumentConversionResultType? resultType) + => resultType switch + { + ArgumentConversionResultType.NoArgument => ValueResultOutcome.NoArgument, + ArgumentConversionResultType.Successful => ValueResultOutcome.Success, + _ => ValueResultOutcome.HasErrors + }; +} diff --git a/src/System.CommandLine/Parsing/ValueResultOutcome.cs b/src/System.CommandLine/Parsing/ValueResultOutcome.cs new file mode 100644 index 0000000000..f4a465a59b --- /dev/null +++ b/src/System.CommandLine/Parsing/ValueResultOutcome.cs @@ -0,0 +1,11 @@ +// 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; + +public enum ValueResultOutcome +{ + NoArgument, // NoArgumentConversionResult + Success, // SuccessfulArgumentConversionResult + HasErrors, // FailedArgumentConversionResult, there are one or more errors +} \ No newline at end of file diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index fa596533f6..9c2d9cbc78 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -27,6 +27,9 @@ + + + @@ -58,6 +61,7 @@ + From baefb1f353d3a120df6ec87f97ca8c8ee168ee76 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Fri, 5 Apr 2024 16:31:13 -0400 Subject: [PATCH 047/150] Clean up during PR review --- src/System.CommandLine.Tests/ParserTests.cs | 15 +- .../TokenizerTests.cs | 1 - src/System.CommandLine/ParseResult.cs | 338 +++++++++--------- .../Parsing/ArgumentResult.cs | 11 +- src/System.CommandLine/Parsing/CliToken.cs | 7 +- .../Parsing/CommandValueResult.cs | 8 +- .../Parsing/OptionResult.cs | 2 +- .../Parsing/SymbolResultTree.cs | 2 +- .../Parsing/ValueResultExtensions.cs | 17 - .../System.CommandLine.csproj | 1 - 10 files changed, 195 insertions(+), 207 deletions(-) delete mode 100644 src/System.CommandLine/Parsing/ValueResultExtensions.cs diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 4cdaa9b91d..c849292420 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -562,8 +562,7 @@ public void An_inner_command_with_the_same_name_does_not_capture() [Fact] public void When_nested_commands_all_accept_arguments_then_the_nearest_captures_the_arguments() { - var command = new CliCommand( - "outer") + var command = new CliCommand("outer") { new CliArgument("arg1"), new CliCommand("inner") @@ -803,11 +802,12 @@ public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_a } }; - ParseResult result = CliParser.Parse(command, new[] { "the-command", - "complete", - "--position", - "7", - "the-command" }); + ParseResult result = CliParser.Parse(command, new[] { + "the-command", + "complete", + "--position", + "7", + "the-command" }); CommandResult completeResult = result.CommandResult; @@ -975,7 +975,6 @@ public void Command_default_argument_value_does_not_override_parsed_value() } */ - [Fact] public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_tokens() { diff --git a/src/System.CommandLine.Tests/TokenizerTests.cs b/src/System.CommandLine.Tests/TokenizerTests.cs index 854ec3125a..0184e990bf 100644 --- a/src/System.CommandLine.Tests/TokenizerTests.cs +++ b/src/System.CommandLine.Tests/TokenizerTests.cs @@ -23,7 +23,6 @@ public void The_tokenizer_can_handle_single_option() List tokens = null; List errors = null; Tokenizer.Tokenize(args, command, new CliConfiguration(command), true, out tokens, out errors); - Tokenizer.Tokenize(args, command, new CliConfiguration(command), true, out tokens, out errors); tokens .Skip(1) diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 32a51061bc..f395cbca81 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -17,42 +17,42 @@ public sealed class ParseResult private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); private readonly CommandResult _rootCommandResult; -// TODO: unmatched tokens, invocation, completion -/* - private readonly IReadOnlyList _unmatchedTokens; - private CompletionContext? _completionContext; - private readonly CliAction? _action; - private readonly List? _preActions; -*/ + // TODO: unmatched tokens, invocation, completion + /* + private readonly IReadOnlyList _unmatchedTokens; + private CompletionContext? _completionContext; + private readonly CliAction? _action; + private readonly List? _preActions; + */ internal ParseResult( CliConfiguration configuration, -// TODO: determine how rootCommandResult and commandResult differ + // TODO: determine how rootCommandResult and commandResult differ CommandResult rootCommandResult, CommandResult commandResult, Dictionary valueResults, List tokens, -// TODO: unmatched tokens -// List? unmatchedTokens, + // TODO: unmatched tokens + // List? unmatchedTokens, List? errors, -// TODO: commandLineText should be string array + // TODO: commandLineText should be string array string? commandLineText = null //, -// TODO: invocation -/* - CliAction? action = null, - List? preActions = null) -*/ + // TODO: invocation + /* + CliAction? action = null, + List? preActions = null) + */ ) { Configuration = configuration; _rootCommandResult = rootCommandResult; CommandResult = commandResult; - this.valueResultDictionary = valueResults; + valueResultDictionary = valueResults; // TODO: invocation -/* - _action = action; - _preActions = preActions; -*/ + /* + _action = action; + _preActions = preActions; + */ // skip the root command when populating Tokens property if (tokens.Count > 1) @@ -70,16 +70,16 @@ internal ParseResult( CommandLineText = commandLineText; -// TODO: unmatched tokens -// _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; - + // TODO: unmatched tokens + // _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; + Errors = errors is not null ? errors : Array.Empty(); } -// TODO: check that constructing empty ParseResult directly is correct -/* - internal static ParseResult Empty() => new CliRootCommand().Parse(Array.Empty()); -*/ + // TODO: check that constructing empty ParseResult directly is correct + /* + internal static ParseResult Empty() => new CliRootCommand().Parse(Array.Empty()); + */ /// /// A result indicating the command specified in the command line input. @@ -166,7 +166,9 @@ CommandLineText is null */ public ValueResult? GetValueResult(CliSymbol symbol) - => valueResultDictionary[symbol]; + => valueResultDictionary.TryGetValue(symbol, out var result) + ? result + : null; /// /// Gets the result, if any, for the specified argument. @@ -192,15 +194,15 @@ CommandLineText is null public OptionResult? GetResult(CliOption option) => _rootCommandResult.GetResult(option); -// TODO: Directives -/* - /// - /// Gets the result, if any, for the specified directive. - /// - /// The directive for which to find a result. - /// A result for the specified directive, or if it was not provided. - public DirectiveResult? GetResult(CliDirective directive) => _rootCommandResult.GetResult(directive); -*/ + // TODO: Directives + /* + /// + /// Gets the result, if any, for the specified directive. + /// + /// The directive for which to find a result. + /// A result for the specified directive, or if it was not provided. + public DirectiveResult? GetResult(CliDirective directive) => _rootCommandResult.GetResult(directive); + */ /// /// Gets the result, if any, for the specified symbol. /// @@ -209,169 +211,169 @@ CommandLineText is null public SymbolResult? GetResult(CliSymbol symbol) => _rootCommandResult.SymbolResultTree.TryGetValue(symbol, out SymbolResult? result) ? result : null; -// TODO: completion, invocation -/* - /// - /// Gets completions based on a given parse result. - /// - /// The position at which completions are requested. - /// A set of completions for completion. - public IEnumerable GetCompletions( - int? position = null) - { - SymbolResult currentSymbolResult = SymbolToComplete(position); - - CliSymbol currentSymbol = currentSymbolResult switch - { - ArgumentResult argumentResult => argumentResult.Argument, - OptionResult optionResult => optionResult.Option, - DirectiveResult directiveResult => directiveResult.Directive, - _ => ((CommandResult)currentSymbolResult).Command - }; - - var context = GetCompletionContext(); + // TODO: completion, invocation + /* + /// + /// Gets completions based on a given parse result. + /// + /// The position at which completions are requested. + /// A set of completions for completion. + public IEnumerable GetCompletions( + int? position = null) + { + SymbolResult currentSymbolResult = SymbolToComplete(position); - if (position is not null && - context is TextCompletionContext tcc) - { - context = tcc.AtCursorPosition(position.Value); - } + CliSymbol currentSymbol = currentSymbolResult switch + { + ArgumentResult argumentResult => argumentResult.Argument, + OptionResult optionResult => optionResult.Option, + DirectiveResult directiveResult => directiveResult.Directive, + _ => ((CommandResult)currentSymbolResult).Command + }; - var completions = currentSymbol.GetCompletions(context); + var context = GetCompletionContext(); - string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult - ? OptionsWithArgumentLimitReached(commandResult) - : Array.Empty(); + if (position is not null && + context is TextCompletionContext tcc) + { + context = tcc.AtCursorPosition(position.Value); + } - completions = - completions.Where(item => optionsWithArgumentLimitReached.All(s => s != item.Label)); + var completions = currentSymbol.GetCompletions(context); - return completions; + string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult + ? OptionsWithArgumentLimitReached(commandResult) + : Array.Empty(); - static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => - commandResult - .Children - .OfType() - .Where(c => c.IsArgumentLimitReached) - .Select(o => o.Option) - .SelectMany(c => new[] { c.Name }.Concat(c.Aliases)) - .ToArray(); - } + completions = + completions.Where(item => optionsWithArgumentLimitReached.All(s => s != item.Label)); - /// - /// Invokes the appropriate command handler for a parsed command line input. - /// - /// A token that can be used to cancel an invocation. - /// A task whose result can be used as a process exit code. - public Task InvokeAsync(CancellationToken cancellationToken = default) - => InvocationPipeline.InvokeAsync(this, cancellationToken); + return completions; - /// - /// Invokes the appropriate command handler for a parsed command line input. - /// - /// A value that can be used as a process exit code. - public int Invoke() - { - var useAsync = false; + static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => + commandResult + .Children + .OfType() + .Where(c => c.IsArgumentLimitReached) + .Select(o => o.Option) + .SelectMany(c => new[] { c.Name }.Concat(c.Aliases)) + .ToArray(); + } - if (Action is AsynchronousCliAction) - { - useAsync = true; - } - else if (PreActions is not null) - { - for (var i = 0; i < PreActions.Count; i++) + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// A token that can be used to cancel an invocation. + /// A task whose result can be used as a process exit code. + public Task InvokeAsync(CancellationToken cancellationToken = default) + => InvocationPipeline.InvokeAsync(this, cancellationToken); + + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// A value that can be used as a process exit code. + public int Invoke() { - var action = PreActions[i]; - if (action is AsynchronousCliAction) + var useAsync = false; + + if (Action is AsynchronousCliAction) { useAsync = true; - break; } - } - } + else if (PreActions is not null) + { + for (var i = 0; i < PreActions.Count; i++) + { + var action = PreActions[i]; + if (action is AsynchronousCliAction) + { + useAsync = true; + break; + } + } + } - if (useAsync) - { - return InvocationPipeline.InvokeAsync(this, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); - } - else - { - return InvocationPipeline.Invoke(this); - } - } + if (useAsync) + { + return InvocationPipeline.InvokeAsync(this, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + } + else + { + return InvocationPipeline.Invoke(this); + } + } - /// - /// Gets the for parsed result. The handler represents the action - /// that will be performed when the parse result is invoked. - /// - public CliAction? Action => _action ?? CommandResult.Command.Action; + /// + /// Gets the for parsed result. The handler represents the action + /// that will be performed when the parse result is invoked. + /// + public CliAction? Action => _action ?? CommandResult.Command.Action; - internal IReadOnlyList? PreActions => _preActions; + internal IReadOnlyList? PreActions => _preActions; - private SymbolResult SymbolToComplete(int? position = null) - { - var commandResult = CommandResult; + private SymbolResult SymbolToComplete(int? position = null) + { + var commandResult = CommandResult; - var allSymbolResultsForCompletion = AllSymbolResultsForCompletion(); + var allSymbolResultsForCompletion = AllSymbolResultsForCompletion(); - var currentSymbol = allSymbolResultsForCompletion.Last(); + var currentSymbol = allSymbolResultsForCompletion.Last(); - return currentSymbol; + return currentSymbol; - IEnumerable AllSymbolResultsForCompletion() - { - foreach (var item in commandResult.AllSymbolResults()) - { - if (item is CommandResult command) + IEnumerable AllSymbolResultsForCompletion() { - yield return command; + foreach (var item in commandResult.AllSymbolResults()) + { + if (item is CommandResult command) + { + yield return command; + } + else if (item is OptionResult option) + { + if (WillAcceptAnArgument(this, position, option)) + { + yield return option; + } + } + } } - else if (item is OptionResult option) + + static bool WillAcceptAnArgument( + ParseResult parseResult, + int? position, + OptionResult optionResult) { - if (WillAcceptAnArgument(this, position, option)) + if (optionResult.Implicit) { - yield return option; + return false; } - } - } - } - static bool WillAcceptAnArgument( - ParseResult parseResult, - int? position, - OptionResult optionResult) - { - if (optionResult.Implicit) - { - return false; - } + if (!optionResult.IsArgumentLimitReached) + { + return true; + } - if (!optionResult.IsArgumentLimitReached) - { - return true; - } + var completionContext = parseResult.GetCompletionContext(); - var completionContext = parseResult.GetCompletionContext(); + if (completionContext is TextCompletionContext textCompletionContext) + { + if (position.HasValue) + { + textCompletionContext = textCompletionContext.AtCursorPosition(position.Value); + } - if (completionContext is TextCompletionContext textCompletionContext) - { - if (position.HasValue) - { - textCompletionContext = textCompletionContext.AtCursorPosition(position.Value); - } + if (textCompletionContext.WordToComplete.Length > 0) + { + var tokenToComplete = parseResult.Tokens.Last(t => t.Value == textCompletionContext.WordToComplete); - if (textCompletionContext.WordToComplete.Length > 0) - { - var tokenToComplete = parseResult.Tokens.Last(t => t.Value == textCompletionContext.WordToComplete); + return optionResult.Tokens.Contains(tokenToComplete); + } + } - return optionResult.Tokens.Contains(tokenToComplete); + return !optionResult.IsArgumentLimitReached; } } - - return !optionResult.IsArgumentLimitReached; - } - } - */ + */ } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/ArgumentResult.cs index 5ec24b3ef5..133545134a 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/ArgumentResult.cs @@ -34,7 +34,7 @@ public ValueResult ValueResult var conversionValue = GetArgumentConversionResult().Value; var locations = Tokens.Select(token => token.Location).ToArray(); //TODO: Remove this wrapper later - _valueResult = new ValueResult(Argument, conversionValue, locations, ValueResultExtensions.GetValueResultOutcome(GetArgumentConversionResult()?.Result)); // null is temporary here + _valueResult = new ValueResult(Argument, conversionValue, locations, ArgumentResult.GetValueResultOutcome(GetArgumentConversionResult()?.Result)); // null is temporary here } return _valueResult; } @@ -47,7 +47,6 @@ public ValueResult ValueResult internal bool ArgumentLimitReached => Argument.Arity.MaximumNumberOfValues == (_tokens?.Count ?? 0); - internal ArgumentConversionResult GetArgumentConversionResult() => _conversionResult ??= ValidateAndConvert(useValidators: true); @@ -236,5 +235,13 @@ ArgumentConversionResult ReportErrorIfNeeded(ArgumentConversionResult result) /// private SymbolResult AppliesToPublicSymbolResult => Parent is OptionResult optionResult ? optionResult : this; + + internal static ValueResultOutcome GetValueResultOutcome(ArgumentConversionResultType? resultType) + => resultType switch + { + ArgumentConversionResultType.NoArgument => ValueResultOutcome.NoArgument, + ArgumentConversionResultType.Successful => ValueResultOutcome.Success, + _ => ValueResultOutcome.HasErrors + }; } } diff --git a/src/System.CommandLine/Parsing/CliToken.cs b/src/System.CommandLine/Parsing/CliToken.cs index ce7e1aeb65..5ee2bea4e8 100644 --- a/src/System.CommandLine/Parsing/CliToken.cs +++ b/src/System.CommandLine/Parsing/CliToken.cs @@ -15,10 +15,11 @@ internal sealed class CliToken : IEquatable public static CliToken CreateFromOtherToken(CliToken otherToken, string? arg, Location location) => new(arg, otherToken.Type, otherToken.Symbol, location); + /* /// The string value of the token. /// The type of the token. /// The symbol represented by the token - /* + public CliToken(string? value, CliTokenType type, CliSymbol symbol) { Value = value ?? ""; @@ -28,6 +29,10 @@ public CliToken(string? value, CliTokenType type, CliSymbol symbol) } */ + /// The string value of the token. + /// The type of the token. + /// The symbol represented by the token + /// The location of the token in the args array or a response file internal CliToken(string? value, CliTokenType type, CliSymbol? symbol, Location location) { Value = value ?? ""; diff --git a/src/System.CommandLine/Parsing/CommandValueResult.cs b/src/System.CommandLine/Parsing/CommandValueResult.cs index 0de61e9953..01ea7085d2 100644 --- a/src/System.CommandLine/Parsing/CommandValueResult.cs +++ b/src/System.CommandLine/Parsing/CommandValueResult.cs @@ -1,17 +1,11 @@ // 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; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace System.CommandLine.Parsing; public class CommandValueResult { - IEnumerable ValueResults { get; } = new List(); - - + public IEnumerable ValueResults { get; } = new List(); } diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/OptionResult.cs index 24b7e765ef..9892067216 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/OptionResult.cs @@ -37,7 +37,7 @@ public ValueResult ValueResult var conversionValue = ArgumentConversionResult.Value; var locations = Tokens.Select(token => token.Location).ToArray(); //TODO: Remove this wrapper later - _valueResult = new ValueResult(Option, conversionValue, locations, ValueResultExtensions.GetValueResultOutcome(ArgumentConversionResult?.Result)); // null is temporary here + _valueResult = new ValueResult(Option, conversionValue, locations, ArgumentResult.GetValueResultOutcome(ArgumentConversionResult?.Result)); // null is temporary here } return _valueResult; } diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 2f15c15a7e..f213181ba0 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -72,7 +72,7 @@ internal Dictionary GetValueResultDictionary() var dict = new Dictionary(); foreach (KeyValuePair pair in this) { - var result = pair.Value; + var result = pair.Value; if (result is OptionResult optionResult) { dict.Add(pair.Key, optionResult.ValueResult); diff --git a/src/System.CommandLine/Parsing/ValueResultExtensions.cs b/src/System.CommandLine/Parsing/ValueResultExtensions.cs deleted file mode 100644 index 2fa36d54b9..0000000000 --- a/src/System.CommandLine/Parsing/ValueResultExtensions.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. - -using System.CommandLine.Binding; - -namespace System.CommandLine.Parsing; - -internal static class ValueResultExtensions -{ - internal static ValueResultOutcome GetValueResultOutcome(ArgumentConversionResultType? resultType) - => resultType switch - { - ArgumentConversionResultType.NoArgument => ValueResultOutcome.NoArgument, - ArgumentConversionResultType.Successful => ValueResultOutcome.Success, - _ => ValueResultOutcome.HasErrors - }; -} diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 9c2d9cbc78..6cd32ef4d8 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -28,7 +28,6 @@ - From dc2cd943caf1225b5fa6380e08399f00099d22a8 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 6 Apr 2024 09:44:36 -0400 Subject: [PATCH 048/150] Updates based on CI issues --- .../Directives/DiagramSubsystem.cs | 6 +++--- src/System.CommandLine.Subsystems/ValueSubsystem.cs | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 9702108ffd..e4eabf2dfe 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -66,10 +66,10 @@ private static void Diagram( builder.Append('!'); } + // TODO: Directives + /* switch (symbolResult) { - // TODO: Directives - /* case DirectiveResult { Directive: not DiagramDirective }: break; */ @@ -174,7 +174,7 @@ private static void Diagram( break; } } - */ } + */ } } diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index 62955cd8d6..1f2757bd51 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -26,10 +26,11 @@ AnnotationAccessor Explicit void SetCalculated(CliSymbol symbol, Func factory) => SetAnnotation(symbol, ValueAnnotations.Calculated, factory); - Func GetCalculated(CliSymbol symbol) - => TryGetAnnotation>(symbol, ValueAnnotations.Calculated, out var value) - ? value - : null; + Func? GetCalculatedValue(CliSymbol symbol) + => TryGetAnnotation>(symbol, ValueAnnotations.Calculated, out var value) + ? value + : null; + AnnotationAccessor> Calculated => new(this, ValueAnnotations.Calculated); From 34657a8e40d413b3532b5612fbd1aa48126350fb Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 4 Apr 2024 01:56:26 -0400 Subject: [PATCH 049/150] Add subsystem extensibility doc for reference The shape of some of the APIs has been superseded since this was originally written but the conceptual overview is still useful --- docs/proposals/extensibility-overview.md | 369 +++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 docs/proposals/extensibility-overview.md diff --git a/docs/proposals/extensibility-overview.md b/docs/proposals/extensibility-overview.md new file mode 100644 index 0000000000..af6c6fd983 --- /dev/null +++ b/docs/proposals/extensibility-overview.md @@ -0,0 +1,369 @@ +# System.CommandLine Extensibility + +## Overview + +`System.CommandLine` is still in preview, yet it had 1.3 million downloads over the past 6 weeks, and is a key dependency of the .NET CLI. There is no other stable first-party story for handling command-line options. This is a problem for authors of command-line tools in the .NET team and in the community, as handling command-line input in a way that is correct and consistent with other tools in the ecosystem is nontrivial. + +However, the main reason `System.CommandLine` is in preview is that it has not passed API review and has some open questions and opinionated decisions that have not been fully validated with customers. Due to the large amount of functionality, this is currently difficult to resolve. Some functionality is certain to change, but this is commingled with the parser, which cannot change behavior without breaking scripts that use CLI tools. + +This document proposes a progressive approach to landing `System.CommandLine` by breaking it up into layered pieces that can be individually finalized. These pieces are intended to be composable, with tightly scoped default implementations. Developers with custom needs and developers of other command-line libraries (such as `Spectre.Console`) will be able to build on top of these pieces and/or provide richer drop-in replacements. + +## Layering + +### Parser + +Implementing command-line parsing in a complete and consistent way is difficult due to many details such as POSIX behavior, aliases, option bundling, escaping, arity, etc. `System.CommandLine` has a robust and well-validated CLI parser, but it is somewhat coupled with `System.CommandLine`’s implementation of higher-level concerns such as validation, help, and invocation. + +The parser has an API for constructing a “grammar”, a tree of nodes describing the CLI’s commands, options and arguments. The parser uses this grammar to parse the args array, producing a parse result. + +The parser layer is a low-level API that is not intended to be replaceable and is minimally extensible. It’s a foundational component for higher-level APIs, third-party command-line libraries and developers with custom needs, providing them with a command-line parsing implementation that is complete and correct. This will help promote consistent command-line parsing behavior across the .NET ecosystem. + +#### Parse Result + +The parser produces a ParseResult that is a structured representation of the argument array analogous to an AST. It contains `CommandResult`, `OptionResult` and `ArgumentResult` objects that represent the commands, arguments and options that were parsed from the args array and preserve the order from the args array. Commands, options and arguments that were defined in the grammar but were not found in the args array will not be present in the `ParseResult`. The result is purely a representation of what was parsed, and determining the value of a specific option or argument (including handling of default values) is the responsibility of higher layers. + +These nodes have `ArgsOffset` and `ArgsLength` properties that indicate the portion of the args array from which they were parsed. This allows advanced scenarios such as printing errors with a position marker. + +If an option has an optional value, such as bool options treating `--enabled` as equivalent to `--enabled=true`, then the `Option` will have a property indicating whether this optional value was provided or not. + +Determining the value of a specific option or argument may involve inspecting other options, or options on parent commands, or using a default value. This will generally be done via the subsystem extensibility layer. + +The result also contains a collection of any errors that were found during parsing. These errors use a Roslyn-like descriptor model, where the prototypical error that defines the format string and error code is separate from the error instance, which contains a reference to the descriptor along with position data and format data. + +#### Type Conversion + +The parser will support C# primitive types in its `Option` and `Argument`. We may also choose to add support for commonly used types such as `DateTime`, `FileInfo`, and `DirectoryInfo`. + +Developers will be able to use other types if they provide a converter. However, this type conversion will not use the current CustomParser extensibility model, as it exposes a low-level Symbol/Token API that we would like to keep internal for now. Instead, it will have a simple, reflection-free type converter that allows will allow `Option` and `Argument` to be used with non-primitive types, without exposing a complex symbol/token API. This model would allow converting any primitive type or array of primitive types: + +```csharp +delegate TOutput TypeConverter(TInput input); + +Option locationOpt = new Option(“--location”) { + TypeConverter = (int[] s) => new Point(s[0],s[1])) +} +``` + +A standard `ParserErrorException` exception type would be used for returning user-visible error messages, and the parser would catch these and add them to its error collection. For convenience, the parser would also collect all other exceptions from type conversion and add a generic “Invalid format” error. + +Type converters are only expected to perform parsing and construction. They are not expected to provide any other kind of validation, as that should be handled in the validation subsystem, which is also responsible for printing any `ParserErrors` in the `ParseResult`. + +#### Reusable Type Converters + +Perhaps we could also allow registering these type converters on the RootCommand so that they do not need to be provided for every option/argument that uses the same type. + +```csharp + +var rootCommand = new RootCommand() + .WithTypeConverter(s => new Point(s[0],s[1])); +``` + +This would also allow libraries to provide helper methods to register converters on the RootCommand: + +```csharp +static RootCommand WithPointConverter(this RootCommand rootCommand) + => root.WithTypeConverter(s => new Point(s[0],s[1])); +``` + +Alternatively, developers using might choose to subclass the option or argument type: + +```csharp +class PointOption : Option { + public PointOption(string name) : base(name) { + TypeConverter = s => new Point(s[0],s[1])); + } +} +``` + +### Subsystems + +A subsystem operates on the parse result and performs some action based on the parser grammar and parse result. The core subsystems are help, validation, invocation, and completion. Developers using the low-level parser API may use any or all the subsystems directly, and higher-level command-line APIs may use subsystems internally. + +The subsystems envisaged are: + +* Error handling: print formatted errors. +* Help: set help information, handle the `--help` option, and print formatted help. +* Completion: handle the completion option. +* Validation: set constraints, and check values respect constraints. +* Invocation: set handler delegates, and dispatch to the correct one. +* Defaults: set default values, and determine values for all arguments and options that take these defaults into account. + +Subsystems are intended to allow drop-in replacements that go beyond the functionality of the default implementations. For example, an alternate help subsystem may format the help output differently. A developer may obtain an alternate subsystem from a NuGet package or implement their own. Higher-level command-line APIs are expected to use relevant subsystems internally and allow developers to optionally override them with alternate subsystems. + +Subsystems may require or be influenced by additional information associated with parser nodes. For example: + +* The invocation subsystem requires handler delegates to be attached to command grammar nodes so they can be invoked when that command is present in the parse result. +* The help subsystem’s output can be enriched by adding help descriptions to the command, option and argument grammar nodes. +* An alternate help subsystem may support additional information such as examples or hyperlinks. +* The existing System.CommandLine.NamingConventionBinder could become an alternate invocation layer, allowing strongly typed command handler delegates with parameters corresponding to the command’s options and arguments, and binding them automatically when the command is invoked. + +These subsystem annotations do not influence parsing so do not need to be coupled with the parser layer. High-performance scenarios may wish to lazily provide some annotations, such as only loading help descriptions when help is invoked, and alternate subsystems may define and use additional annotations. For this reason, an extensible model for subsystem annotations will be provided at the subsystem layer and is detailed later in this document. + +Although alternate subsystems may have additional annotations, they are expected to use the annotations of the default subsystems where possible, so that when alternate subsystems are dropped into an existing application, they use any relevant information that the developer has already provided. + +### Binding/Model + +A binding/model layer wraps the parser and subsystem layers in a high-level, user-friendly API. In the long term, most developers of CLI apps should be using a model/binding layer. A binding/model layer is by nature opinionated, and there are many different idioms it could use. However, they can all build on top of the parser and subsystems layers, and allow drop-in replacement of subsystems. + +An example of a model/binding layer is `System.CommandLine`’s `DragonFruit` API, which allows developers to define their CLI by writing a Main method with strongly typed parameters corresponding to the CLI’s options and arguments. Using a Roslyn generator, DragonFruit internally constructs a parser grammar using the method’s signature, converting default parameter values into default option values, and converting doc comments into help descriptions. On invocation it binds the command-line options and arguments to the method’s arguments and invokes the method. + +## Stabilization Status + +The parser layer’s API is almost shovel-ready for inclusion in the BCL in .NET 9, and we should be able to re-use much of the implementation and tests from System.CommandLine. + +The next target will be to stabilize the subsystems. The subsystems may not become part of the BCL but will be a stable package. When the .NET CLI migrates to this package, this will eliminate its preview dependency, as it does not use a binding/model layer. +The default subsystems will be a straightforward transformation of the existing functionality used by the .NET CLI. However, the subsystem API and annotation APIs will require some discussion. + +The model/binding layer requires more experimentation with different forms of binding/model layer to validate with developers before committing to using one for the Console App templates in Visual Studio and the .NET SDK. We may end up with multiple, and there will no doubt be third party ones. + +## Subsystem API Pattern + +The subsystems follow a common pattern for initialization, annotations, and invocation. + +### Initialization + +All subsystems have an initialization call to create the subsystem and apply any settings. This initialization call may require a RootCommand instance so it can add options, such as the --help option required by the help subsystem. + +One question here is whether subsystems should be locals or should be attached to the `RootCommand` or some other collection such as a new `CliPipeline` class. Ideally the subsystems would not be stored on the `RootCommand` as that would require either putting the concepts of subsystems in the parser layer (e.g. an abstract `CliSubsystem` class and a `Dictionary` on `RootCommand`) or having a completely generic `PropertyBag`-like object storage mechanism on `RootCommand`, which is not generally considered a good pattern for the BCL. As developers will need to create an instance of a subsystem to opt into using that subsystem, it seems reasonable to store them in locals. The value of storing the subsystem instances in a standard place would be if that made it easier for extension methods to locate the subsystem instance. + +The developer must be able to provide an instance of an alternate subsystem, or an instance of the subsystem configured with custom options. The subsystem may also have an optional parameter for an annotation provider, which allows performance-sensitive developers to perform lazy lookup of annotations instead of setting them upfront. + +For example, a local-based initialization call might simply be a constructor call: + +```csharp +var help = new MyAlternateHelpSubsystem(helpAnnotationProvider); +``` + +### Annotations + +#### Annotation Storage + +A subsystem must also provide methods to annotate `CliSymbol` grammar nodes (`CliCommand`, `CliOption` and `CLiArgument`) with arbitrary string-keyed data. An open question is how this data should be attached. + +It would be desirable to allow setting symbol-specific annotations directly on the symbol, e.g. + +```csharp +command.SetDescription("This is a description"); +``` + +However, this would require the parser layer to be aware of the concept of subsystem annotations and to expose a `PropertyBag`-like model for storage of arbitrary data, which is very unlikely to pass BCL API review. Alternatively the subsystem layer could add subclasses for all the `CliSymbol` derived classes to store this data, but this creates a bifurcation in the usage of the parser API. The last option would be to use a hidden static `ConditionalWeakTable` to associate annotation data with symbol instances, but magically storing instance data in a hidden static field is not a good pattern, and has problematic implications around performance and threading. + +Instead, we could make each subsystem responsible for storing its own annotation data. For example, the base `CliSubsystem` could expose the following annotation API: + +```csharp +void SetAnnotation(CliSymbol symbol, string id, T value); +T GetAnnotation(CliSymbol symbol, string id); +``` + +These would internally store the annotation values on a dictionary keyed on the symbol and the annotation ID. + +#### Annotation Accessors + +Developers would not expected to use these base annotation accessors directly unless they are writing an alternate subsystem that has its own additional annotations. The default subsystem and alternate subsystems should provider wrapper methods for specific annotations. + +For example, for help descriptions, the `HelpSubsystem` could have the following accessors: + +```csharp +void SetDescription(CliSymbol symbol, string description) + => SetAnnotation(symbol, HelpAnnotations.Description, description); +string GetDescription(CliSymbol symbol) + => GetAnnotation(symbol, HelpAnnotations.Description); +``` + +There would also be static classes defining the IDs of well-known annotations for use by subsystems and annotation providers, such as `HelpAnnotations.Description`. + +#### Fluent Annotations + +Unfortunately it is not easy to add fluent helpers such as `command.WithHelpDescription(“Some description”)` as such an extension method would not be able to locate the annotation storage unless the annotations were stored on the symbol or accessible via a hidden static `ConditionalWeakTable`, which are problematic for the reasons described earlier. + +Even storing the subsystem on the `RootCommand` would not help with this, as in the following example, the `SetHelpDescription` extension methods would not have access to the `RootCommand` instance as the `Command`’s parent is not set until after the `WithHelpDescription` extension method is called: + +```csharp +rootCommand.Add( + new Command(“--hello”) + .WithHelpDescription(“Hello”)); +``` + +However, a different approach to annotation wrappers would enable a pattern for fluently setting annotations on grammar nodes when constructing the grammar. + +The following `AnnotationAccessor` wrapper struct encapsulates a reference to the subsystem and the annotation ID: + +```csharp +record struct AnnotationAccessor (Subsystem Subsystem, string Id) { + public void Set(CliSymbol symbol, T value) => subsystem.SetAnnotation(symbol, Id, value); + public T Get(CliSymbol symbol) => subsystem.GetAnnotation(symbol, Id); +} +``` + +Subsystems would be expected to provide properties that expose instances of this wrapper for individual annotations: + +```csharp +AnnotationAccessor Description => new (this, HelpAnnotations.Description); +``` + +This would allow setting an annotation value for a node via these annotation wrappers with the following pattern, replacing the earlier `SetDescription` wrapper method pattern: + +```csharp +help.Description.Set(thingCommand, “This is a thing”); +``` + +Using this pattern instead of the earlier `Set`/`GetDescription` style wrappers would allow the implementation of the following extension method on the grammar nodes: + +```csharp +static Command With(this CliSymbol symbol, AnnotationAccessor accessor, T value) + => accessor.SetValue(symbol, value); +``` + +This extension method would allow fluently setting the help description in a relatively discoverable and easily readable way: + +```csharp +var rootCommand = new RootCommand(); +var help = new HelpSubsystem(); +rootCommand.Add ( + new Command (“greet”) + .With(help.Description, “Greet the user”) +); +``` + +#### Annotation Providers + +The annotation provider model allows performance-sensitive developers to opt into lazily fetching annotations when needed. Developers may provide an instance of this provider to a subsystem when initializing the subsystem. + +```csharp +interface AnnotationProvider { + Get(CliSymbol symbol, string id, object value); +} +``` + +An implementation of one of these methods might look as follows: + +```csharp +GetCommand command, string id, object value) + => (command.Name, id) switch { + (“greet”, HelpAnnotation.Description) => “Greet the user”, + _ => null +}; +``` + +It would even be possible to implement a source generator for optimizing CLI apps by converting fluent annotations into lazy annotations. It would collect values passed to the `With(Annotation,T)` extension method, generate annotation provider implementations that provide those value lazily, and elide the `With` method calls with an interceptor. + +### Subsystem Invocation + +Subsystems provide a method that must be called after parsing to invoke the subsystem. Invocation uses the `ParseResult`, subsystem annotations, and any settings provided when initializing the subsystem. + +When invoked subsystems should print any warnings and errors using the error handler subsystem. If not provided an error handler subsystem, they should use the default one, which prints to stderr. An alternate error handler implementation could customize how errors are rendered, or it could collect the errors so that they could be inspected or printed a later point. + +If subsystem invocation determines that the app should be terminated, it should return an `ExitDescriptor`, otherwise `null`. This `ExitDescriptor` encapsulates an exit code and a description of the code’s meaning that is intended to be printed only when showing information about all available exit codes and their meanings. + +For example, the `HelpSubsystem`’s `ShowIfNeeded` invocation method checks whether the parseResult contains the help option, and if so, it prints help based on the grammar and annotations, and returns `ExitDescriptor.Success` to indicate that help was invoked and the program should exit. + +### Subsystem Pipeline + +Here is an example of a full result handling pipeline that uses all the subsystems: + +```csharp +// if there are any parsing errors, print them, and determines +// which error to use for the exit code and return it +if (errorHandler.TryPrintErrors(parseResult) is CliExit exit) { + // ExitDescriptor has implicit cast to exit code int + return error; +} + +// if result contains help option, show help and return success exit. +// may use values from the validation and default value +// subsystem to enrich output. +if (help.ShowIfNeeded(parseResult, validation, defaults, errorHandler) is CliExit exit) { + return exit; +} + +// if result contains completion directive, print completion +// and return success exit. may return an error exit if there is some +// internal error. +if (completion.Handle(parseResult, errorHandler) is CliExit exit) { + return exit; +} + +// validate all values in the parse result and print validation +// errors to the errorHandler. if any errors, return appropriate exit. +if (validation.Validate(parseResult, errorHandler) is CliExit exit) { + return exit +} +// create a collection that can return values for all options +// and arguments, even if they were not present in the ParseResult. +// if any default value delegate throws exceptions, +// print them using the errorHandler, and returns error exit. +if (CliValues.Create(parseResult, defaults, errorHandler, out CliValues values) is CliExit exit) { + return exit; +} + +// determine which handler delegate to use and invoke it. +// depending how the delegate was registered, may pass the values +// collection to the invocation delegate directly, or bind +// the delegate’s arguments to values from this collection. +// returns the exit descriptor returned from the invoked delegate, +// or null if it did not find a delegate to invoke. +if (invocation.Dispatch(parseResult, values, errorHandler) is CliExit exit) { + return exit; +} + +// creates a customized ExitDescriptor that also prints +// a short form of the help +CliExit noCommandExit = help.CreateNoCommandExit (); +errorHandler.WriteError(noCommandExit); +return noCommandExit; +``` + +There would be several extension methods that encapsulate the standard subsystem invocation pipeline shown above. The most important is `Invoke`: + +```csharp +ExitDescriptor Invoke( + this parseResult result, + InvocationSubsystem invocation, + HelpSubsystem? helpSubsystem = null, + DefaultValuesSubsystem? defaultValues = null, + ValidationSubsystem? validationSubsystem = null, + CompletionSubsystem? completion = null, + ErrorHandler? errorHandler = null +) +``` + +Note that the `InvocationSubsystem` subsystem cannot be null for the `Invoke` helper. Note also that the arguments are in the order of most to least likely to be provided , making it more likely arguments can be omitted without passing nulls or using named arguments. + +The `GetValues` variant of this helper omits the invocation subsystem, and returns the CliValues and the command, for users who want to perform invocation manually: + +```csharp +var (exitDescriptor, command, values) = parseResult.GetValues( + help, defaultValues, validation, completion, errorHandler +); +``` + +Note that any of the subsystems passed to these helpers may be null. If the error handle, help, completion, or default value subsystems are null, an instance of the default implementation will be used. The validation and invocation subsystem invocations will be skipped if they are not provided, as they do nothing without annotations so the default instance would be redundant. + +Although most users would use these helpers, some very advanced cases may wish to use any or all of the subsystems in a custom pipeline. The main value of this subsystem model is that apps can use alternate implementations for any or all of the subsystems, either written specifically for the app or obtained from NuGet. + +## End-to-End Example + +Here is an end-to-end example of an entire CLI application that initializes subsystems, constructs a simple command, attaches annotations, and runs the subsystem pipeline: + +```csharp +var rootCommand = new RootCommand(); + +var help = new HelpSubsystem(rootCommand); +var invocation = new InvocationSubsystem(rootCommand); +var defaults = new DefaultValuesSubsystem(rootCommand); + +rootCommand.Add ( + new Command (“greet”, + new Argument(“name”), + .With (help.Description, “The name of the person to greet”), + .With (defaults.Provider, () => Environment.UserName) + ) + .With(help.Description, “Greet the user”) + .With(invocation.Handler, + name => Console.WriteLine($“Hello {name}!”)) +); + +var parseResult = rootCommand.Parse(args); + +return parseResult.Invoke (invocation, help, defaults); +``` From 1f9dcbe7cfb073d5a2cf387195699cd15ab4e614 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Tue, 16 Apr 2024 16:09:58 -0400 Subject: [PATCH 050/150] Whitespace changes in PR response --- .../PipelineTests.cs | 323 ++-- .../VersionSubsystemTests.cs | 197 +-- .../Directives/DiagramSubsystem.cs | 6 +- .../Directives/ResponseSubsystem.cs | 2 +- src/System.CommandLine.Tests/ParserTests.cs | 1483 ++++++++--------- src/System.CommandLine/ParseResult.cs | 330 ++-- .../Parsing/ArgumentResult.cs | 40 +- src/System.CommandLine/Parsing/CliToken.cs | 1 - .../Parsing/CommandResult.cs | 2 +- .../Parsing/ParseOperation.cs | 1 - .../Parsing/StringExtensions.cs | 2 +- .../Parsing/SymbolResultTree.cs | 54 +- .../Parsing/ValueResultOutcome.cs | 2 +- 13 files changed, 1210 insertions(+), 1233 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs index 2c14cc0de4..b99d9f1b72 100644 --- a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs @@ -5,192 +5,193 @@ using System.CommandLine.Parsing; using Xunit; -namespace System.CommandLine.Subsystems.Tests; - -public class PipelineTests +namespace System.CommandLine.Subsystems.Tests { - private static Pipeline GetTestPipeline(VersionSubsystem versionSubsystem) - => new() - { - Version = versionSubsystem - }; - private static CliConfiguration GetNewTestConfiguration() - => new(new CliRootCommand { new CliOption("-x") }); // Add option expected by test data - - private static ConsoleHack GetNewTestConsole() - => new ConsoleHack().RedirectToBuffer(true); - - //private static (Pipeline pipeline, CliConfiguration configuration, ConsoleHack consoleHack) StandardObjects(VersionSubsystem versionSubsystem) - //{ - // var configuration = new CliConfiguration(new CliRootCommand { new CliOption("-x") }); - // var pipeline = new Pipeline - // { - // Version = versionSubsystem - // }; - // var consoleHack = new ConsoleHack().RedirectToBuffer(true); - // return (pipeline, configuration, consoleHack); - //} - - [Theory] - [ClassData(typeof(TestData.Version))] - public void Subsystem_runs_in_pipeline_only_when_requested(string input, bool shouldRun) + public class PipelineTests { - var pipeline = GetTestPipeline(new VersionSubsystem()); - var console = GetNewTestConsole(); + private static Pipeline GetTestPipeline(VersionSubsystem versionSubsystem) + => new() + { + Version = versionSubsystem + }; + private static CliConfiguration GetNewTestConfiguration() + => new(new CliRootCommand { new CliOption("-x") }); // Add option expected by test data + + private static ConsoleHack GetNewTestConsole() + => new ConsoleHack().RedirectToBuffer(true); + + //private static (Pipeline pipeline, CliConfiguration configuration, ConsoleHack consoleHack) StandardObjects(VersionSubsystem versionSubsystem) + //{ + // var configuration = new CliConfiguration(new CliRootCommand { new CliOption("-x") }); + // var pipeline = new Pipeline + // { + // Version = versionSubsystem + // }; + // var consoleHack = new ConsoleHack().RedirectToBuffer(true); + // return (pipeline, configuration, consoleHack); + //} + + [Theory] + [ClassData(typeof(TestData.Version))] + public void Subsystem_runs_in_pipeline_only_when_requested(string input, bool shouldRun) + { + var pipeline = GetTestPipeline(new VersionSubsystem()); + var console = GetNewTestConsole(); - var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); + var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); - exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); - if (shouldRun) - { - console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + if (shouldRun) + { + console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); + } } - } - [Theory] - [ClassData(typeof(TestData.Version))] - public void Subsystem_runs_with_explicit_parse_only_when_requested(string input, bool shouldRun) - { - var pipeline = GetTestPipeline(new VersionSubsystem()); - var console = GetNewTestConsole(); + [Theory] + [ClassData(typeof(TestData.Version))] + public void Subsystem_runs_with_explicit_parse_only_when_requested(string input, bool shouldRun) + { + var pipeline = GetTestPipeline(new VersionSubsystem()); + var console = GetNewTestConsole(); - var result = pipeline.Parse(GetNewTestConfiguration(), input); - var exit = pipeline.Execute(result, input, console); + var result = pipeline.Parse(GetNewTestConfiguration(), input); + var exit = pipeline.Execute(result, input, console); - exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); - if (shouldRun) - { - console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + if (shouldRun) + { + console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); + } } - } - [Theory] - [ClassData(typeof(TestData.Version))] - public void Subsystem_runs_initialize_and_teardown_when_requested(string input, bool shouldRun) - { - var versionSubsystem = new AlternateSubsystems.VersionWithInitializeAndTeardown(); - var pipeline = GetTestPipeline(versionSubsystem); - var console = GetNewTestConsole(); + [Theory] + [ClassData(typeof(TestData.Version))] + public void Subsystem_runs_initialize_and_teardown_when_requested(string input, bool shouldRun) + { + var versionSubsystem = new AlternateSubsystems.VersionWithInitializeAndTeardown(); + var pipeline = GetTestPipeline(versionSubsystem); + var console = GetNewTestConsole(); - var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); + var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); - exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); - versionSubsystem.InitializationWasRun.Should().BeTrue(); - versionSubsystem.ExecutionWasRun.Should().Be(shouldRun); - versionSubsystem.TeardownWasRun.Should().BeTrue(); - } + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + versionSubsystem.InitializationWasRun.Should().BeTrue(); + versionSubsystem.ExecutionWasRun.Should().Be(shouldRun); + versionSubsystem.TeardownWasRun.Should().BeTrue(); + } - [Theory] - [ClassData(typeof(TestData.Version))] - public void Subsystem_works_without_pipeline(string input, bool shouldRun) - { - var versionSubsystem = new VersionSubsystem(); - // TODO: Ensure an efficient conversion as people may copy this code - var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); - var console = GetNewTestConsole(); - var configuration = GetNewTestConfiguration(); - - Subsystem.Initialize(versionSubsystem, configuration, args); - // This approach might be taken if someone is using a subsystem just for initialization - var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); - bool value = parseResult.GetValue("--version"); - - parseResult.Errors.Should().BeEmpty(); - value.Should().Be(shouldRun); - if (shouldRun) + [Theory] + [ClassData(typeof(TestData.Version))] + public void Subsystem_works_without_pipeline(string input, bool shouldRun) { - // TODO: Add an execute overload to avoid checking activated twice - var exit = Subsystem.Execute(versionSubsystem, parseResult, input, console); - exit.Should().NotBeNull(); - exit.ExitCode.Should().Be(0); - exit.Handled.Should().BeTrue(); - console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); + var versionSubsystem = new VersionSubsystem(); + // TODO: Ensure an efficient conversion as people may copy this code + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + var console = GetNewTestConsole(); + var configuration = GetNewTestConfiguration(); + + Subsystem.Initialize(versionSubsystem, configuration, args); + // This approach might be taken if someone is using a subsystem just for initialization + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); + bool value = parseResult.GetValue("--version"); + + parseResult.Errors.Should().BeEmpty(); + value.Should().Be(shouldRun); + if (shouldRun) + { + // TODO: Add an execute overload to avoid checking activated twice + var exit = Subsystem.Execute(versionSubsystem, parseResult, input, console); + exit.Should().NotBeNull(); + exit.ExitCode.Should().Be(0); + exit.Handled.Should().BeTrue(); + console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); + } } - } - - [Theory] - [ClassData(typeof(TestData.Version))] - public void Subsystem_works_without_pipeline_style2(string input, bool shouldRun) - { - var versionSubsystem = new VersionSubsystem(); - var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); - var console = GetNewTestConsole(); - var configuration = GetNewTestConfiguration(); - var expectedVersion = shouldRun - ? TestData.AssemblyVersionString - : ""; - - // Someone might use this approach if they wanted to do something with the ParseResult - Subsystem.Initialize(versionSubsystem, configuration, args); - var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); - var exit = Subsystem.ExecuteIfNeeded(versionSubsystem, parseResult, input, console); - - exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); - console.GetBuffer().Trim().Should().Be(expectedVersion); - } + [Theory] + [ClassData(typeof(TestData.Version))] + public void Subsystem_works_without_pipeline_style2(string input, bool shouldRun) + { + var versionSubsystem = new VersionSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + var console = GetNewTestConsole(); + var configuration = GetNewTestConfiguration(); + var expectedVersion = shouldRun + ? TestData.AssemblyVersionString + : ""; + + // Someone might use this approach if they wanted to do something with the ParseResult + Subsystem.Initialize(versionSubsystem, configuration, args); + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); + var exit = Subsystem.ExecuteIfNeeded(versionSubsystem, parseResult, input, console); - [Theory] - [InlineData("-xy", false)] - [InlineData("--versionx", false)] - public void Subsystem_runs_when_requested_even_when_there_are_errors(string input, bool shouldRun) - { - var versionSubsystem = new VersionSubsystem(); - var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); - var configuration = GetNewTestConfiguration(); + exit.ExitCode.Should().Be(0); + exit.Handled.Should().Be(shouldRun); + console.GetBuffer().Trim().Should().Be(expectedVersion); + } - Subsystem.Initialize(versionSubsystem, configuration, args); - // This approach might be taken if someone is using a subsystem just for initialization - var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); - bool value = parseResult.GetValue("--version"); - parseResult.Errors.Should().NotBeEmpty(); - value.Should().Be(shouldRun); - } + [Theory] + [InlineData("-xy", false)] + [InlineData("--versionx", false)] + public void Subsystem_runs_when_requested_even_when_there_are_errors(string input, bool shouldRun) + { + var versionSubsystem = new VersionSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + var configuration = GetNewTestConfiguration(); - [Fact] - public void Standard_pipeline_contains_expected_subsystems() - { - var pipeline = new StandardPipeline(); - pipeline.Version.Should().BeOfType(); - pipeline.Help.Should().BeOfType(); - pipeline.ErrorReporting.Should().BeOfType(); - pipeline.Completion.Should().BeOfType(); - } + Subsystem.Initialize(versionSubsystem, configuration, args); + // This approach might be taken if someone is using a subsystem just for initialization + var parseResult = CliParser.Parse(configuration.RootCommand, args, configuration); + bool value = parseResult.GetValue("--version"); - [Fact] - public void Normal_pipeline_contains_no_subsystems() - { - var pipeline = new Pipeline(); - pipeline.Version.Should().BeNull(); - pipeline.Help.Should().BeNull(); - pipeline.ErrorReporting.Should().BeNull(); - pipeline.Completion.Should().BeNull(); - } + parseResult.Errors.Should().NotBeEmpty(); + value.Should().Be(shouldRun); + } - [Fact] - public void Subsystems_can_access_each_others_data() - { - // TODO: Explore a mechanism that doesn't require the reference to retrieve data, this shows that it is awkward - var symbol = new CliOption("-x"); - var console = GetNewTestConsole(); - var pipeline = new StandardPipeline - { - Version = new AlternateSubsystems.VersionThatUsesHelpData(symbol) - }; - if (pipeline.Help is null) throw new InvalidOperationException(); - var rootCommand = new CliRootCommand + [Fact] + public void Standard_pipeline_contains_expected_subsystems() { - symbol.With(pipeline.Help.Description, "Testing") - }; + var pipeline = new StandardPipeline(); + pipeline.Version.Should().BeOfType(); + pipeline.Help.Should().BeOfType(); + pipeline.ErrorReporting.Should().BeOfType(); + pipeline.Completion.Should().BeOfType(); + } - pipeline.Execute(new CliConfiguration(rootCommand), "-v", console); + [Fact] + public void Normal_pipeline_contains_no_subsystems() + { + var pipeline = new Pipeline(); + pipeline.Version.Should().BeNull(); + pipeline.Help.Should().BeNull(); + pipeline.ErrorReporting.Should().BeNull(); + pipeline.Completion.Should().BeNull(); + } - console.GetBuffer().Trim().Should().Be($"Testing"); + [Fact] + public void Subsystems_can_access_each_others_data() + { + // TODO: Explore a mechanism that doesn't require the reference to retrieve data, this shows that it is awkward + var symbol = new CliOption("-x"); + var console = GetNewTestConsole(); + var pipeline = new StandardPipeline + { + Version = new AlternateSubsystems.VersionThatUsesHelpData(symbol) + }; + if (pipeline.Help is null) throw new InvalidOperationException(); + var rootCommand = new CliRootCommand + { + symbol.With(pipeline.Help.Description, "Testing") + }; + + pipeline.Execute(new CliConfiguration(rootCommand), "-v", console); + + console.GetBuffer().Trim().Should().Be($"Testing"); + } } } diff --git a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs index 8472cd2bb8..304c12b6da 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs @@ -5,116 +5,117 @@ using Xunit; using System.CommandLine.Parsing; -namespace System.CommandLine.Subsystems.Tests; - -public class VersionSubsystemTests +namespace System.CommandLine.Subsystems.Tests { - [Fact] - public void When_version_subsystem_is_used_the_version_option_is_added_to_the_root() + public class VersionSubsystemTests { - var rootCommand = new CliRootCommand - { - new CliOption("-x") // add option that is expected for the test data used here - }; - var configuration = new CliConfiguration(rootCommand); - var pipeline = new Pipeline + [Fact] + public void When_version_subsystem_is_used_the_version_option_is_added_to_the_root() { - Version = new VersionSubsystem() - }; - - // Parse is used because directly calling Initialize would be unusual - var result = pipeline.Parse(configuration, ""); - - rootCommand.Options.Should().NotBeNull(); - rootCommand.Options - .Count(x => x.Name == "--version") - .Should() - .Be(1); - } - - [Theory] - [ClassData(typeof(TestData.Version))] - public void Version_is_activated_only_when_requested(string input, bool result) - { - CliRootCommand rootCommand = [new CliOption("-x")]; // add random option as empty CLIs are rare - var configuration = new CliConfiguration(rootCommand); - var versionSubsystem = new VersionSubsystem(); - var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); - - Subsystem.Initialize(versionSubsystem, configuration, args); + var rootCommand = new CliRootCommand + { + new CliOption("-x") // add option that is expected for the test data used here + }; + var configuration = new CliConfiguration(rootCommand); + var pipeline = new Pipeline + { + Version = new VersionSubsystem() + }; + + // Parse is used because directly calling Initialize would be unusual + var result = pipeline.Parse(configuration, ""); + + rootCommand.Options.Should().NotBeNull(); + rootCommand.Options + .Count(x => x.Name == "--version") + .Should() + .Be(1); + } + + [Theory] + [ClassData(typeof(TestData.Version))] + public void Version_is_activated_only_when_requested(string input, bool result) + { + CliRootCommand rootCommand = [new CliOption("-x")]; // add random option as empty CLIs are rare + var configuration = new CliConfiguration(rootCommand); + var versionSubsystem = new VersionSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); - var parseResult = CliParser.Parse(rootCommand, input, configuration); - var isActive = Subsystem.GetIsActivated(versionSubsystem, parseResult); + Subsystem.Initialize(versionSubsystem, configuration, args); - isActive.Should().Be(result); - } + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var isActive = Subsystem.GetIsActivated(versionSubsystem, parseResult); - [Fact] - public void Outputs_assembly_version() - { - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var versionSubsystem = new VersionSubsystem(); - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be(Constants.version); - } + isActive.Should().Be(result); + } - [Fact] - public void Outputs_specified_version() - { - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var versionSubsystem = new VersionSubsystem + [Fact] + public void Outputs_assembly_version() { - SpecificVersion = "42" - }; - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be("42"); - } - - [Fact] - public void Outputs_assembly_version_when_specified_version_set_to_null() - { - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var versionSubsystem = new VersionSubsystem + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem(); + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be(Constants.version); + } + + [Fact] + public void Outputs_specified_version() { - SpecificVersion = null - }; - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be(Constants.version); - } - - [Fact] - public void Console_output_can_be_tested() - { - CliConfiguration configuration = new(new CliRootCommand()) - { }; + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem + { + SpecificVersion = "42" + }; + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be("42"); + } + + [Fact] + public void Outputs_assembly_version_when_specified_version_set_to_null() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem + { + SpecificVersion = null + }; + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be(Constants.version); + } + + [Fact] + public void Console_output_can_be_tested() + { + CliConfiguration configuration = new(new CliRootCommand()) + { }; - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var versionSubsystem = new VersionSubsystem(); - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); - consoleHack.GetBuffer().Trim().Should().Be(Constants.version); - } + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var versionSubsystem = new VersionSubsystem(); + Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + consoleHack.GetBuffer().Trim().Should().Be(Constants.version); + } - [Fact] - public void Custom_version_subsystem_can_be_used() - { - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var pipeline = new Pipeline + [Fact] + public void Custom_version_subsystem_can_be_used() { - Version = new AlternateSubsystems.AlternateVersion() - }; - pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); - consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); - } - - [Fact] - public void Custom_version_subsystem_can_replace_standard() - { - var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var pipeline = new StandardPipeline + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var pipeline = new Pipeline + { + Version = new AlternateSubsystems.AlternateVersion() + }; + pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); + consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); + } + + [Fact] + public void Custom_version_subsystem_can_replace_standard() { - Version = new AlternateSubsystems.AlternateVersion() - }; - pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); - consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var pipeline = new StandardPipeline + { + Version = new AlternateSubsystems.AlternateVersion() + }; + pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); + consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); + } } } diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index e4eabf2dfe..a00e8c4734 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -66,8 +66,8 @@ private static void Diagram( builder.Append('!'); } - // TODO: Directives - /* +// TODO: Directives +/* switch (symbolResult) { case DirectiveResult { Directive: not DiagramDirective }: @@ -175,6 +175,6 @@ private static void Diagram( } } } - */ +*/ } } diff --git a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs index 7ccfe817c2..ed43c8d626 100644 --- a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs @@ -26,7 +26,7 @@ public static (List? tokens, List? errors) Replacer(string respo catch { // TODO: Switch to proper errors - return (null, + return (null, errors: [ $"Failed to open response file {responseSourceName}" diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index c849292420..8ef106c8a3 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -72,9 +72,9 @@ public void When_a_token_is_just_a_prefix_then_an_error_is_returned(string prefi var result = CliParser.Parse(rootCommand, prefix); result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument(prefix)); + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument(prefix)); } [Fact] @@ -140,39 +140,39 @@ public void Option_short_forms_can_be_bundled() var result = CliParser.Parse(command, "the-command -xyz"); result.CommandResult - .Children - .Select(o => ((OptionResult)o).Option.Name) - .Should() - .BeEquivalentTo("-x", "-y", "-z"); + .Children + .Select(o => ((OptionResult)o).Option.Name) + .Should() + .BeEquivalentTo("-x", "-y", "-z"); } /* - [Fact] - public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off() + [Fact] + public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off() + { + // TODO: unmatched tokens has been moved, fix + CliRootCommand rootCommand = new CliRootCommand() + { + new CliCommand("the-command") { - // TODO: umatched tokens has been moved, fix - CliRootCommand rootCommand = new CliRootCommand() - { - new CliCommand("the-command") - { - new CliOption("-x"), - new CliOption("-y"), - new CliOption("-z") - } - }; - - CliConfiguration configuration = new (rootCommand) - { - EnablePosixBundling = false - }; + new CliOption("-x"), + new CliOption("-y"), + new CliOption("-z") + } + }; - var result = rootCommand.Parse("the-command -xyz", configuration); + CliConfiguration configuration = new (rootCommand) + { + EnablePosixBundling = false + }; - result.UnmatchedTokens - .Should() - .BeEquivalentTo("-xyz"); - } + var result = rootCommand.Parse("the-command -xyz", configuration); + + result.UnmatchedTokens + .Should() + .BeEquivalentTo("-xyz"); + } */ [Fact] @@ -181,19 +181,19 @@ public void Option_long_forms_do_not_get_unbundled() CliCommand command = new CliCommand("the-command") { - new CliOption("--xyz"), - new CliOption("-x"), - new CliOption("-y"), - new CliOption("-z") + new CliOption("--xyz"), + new CliOption("-x"), + new CliOption("-y"), + new CliOption("-z") }; var result = CliParser.Parse(command, "the-command --xyz"); result.CommandResult - .Children - .Select(o => ((OptionResult)o).Option.Name) - .Should() - .BeEquivalentTo("--xyz"); + .Children + .Select(o => ((OptionResult)o).Option.Name) + .Should() + .BeEquivalentTo("--xyz"); } [Fact] @@ -202,9 +202,9 @@ public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_v var outer = new CliCommand("outer"); outer.Options.Add(new CliOption("-a")); var inner = new CliCommand("inner") - { - new CliArgument("arg") - }; + { + new CliArgument("arg") + }; inner.Options.Add(new CliOption("-b")); inner.Options.Add(new CliOption("-c")); outer.Subcommands.Add(inner); @@ -212,10 +212,10 @@ public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_v ParseResult result = CliParser.Parse(outer, "outer inner -abc"); result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("-abc"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("-abc"); } [Fact] @@ -226,18 +226,18 @@ public void Required_option_arguments_are_not_unbundled() var optionC = new CliOption("-c"); var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-a -bc"); result.GetResult(optionA) - .Tokens - .Should() - .ContainSingle(t => t.Value == "-bc"); + .Tokens + .Should() + .ContainSingle(t => t.Value == "-bc"); } [Fact] @@ -248,11 +248,11 @@ public void Last_bundled_option_can_accept_argument_with_no_separator() var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-abcvalue"); result.GetResult(optionA).Should().NotBeNull(); @@ -272,11 +272,11 @@ public void Last_bundled_option_can_accept_argument_with_equals_separator() var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-abc=value"); result.GetResult(optionA).Should().NotBeNull(); @@ -296,11 +296,11 @@ public void Last_bundled_option_can_accept_argument_with_colon_separator() var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-abc:value"); result.GetResult(optionA).Should().NotBeNull(); @@ -320,11 +320,11 @@ public void Invalid_char_in_bundle_causes_rest_to_be_interpreted_as_value() var optionC = new CliOption("-c") { Arity = ArgumentArity.ExactlyOne }; var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-abvcalue"); result.GetResult(optionA).Should().NotBeNull(); @@ -345,10 +345,10 @@ public void Parser_root_Options_can_be_specified_multiple_times_and_their_argume var animalsOption = new CliOption("-a", "--animals"); var vegetablesOption = new CliOption("-v", "--vegetables"); var rootCommand = new CliRootCommand - { - animalsOption, - vegetablesOption - }; + { + animalsOption, + vegetablesOption + }; var result = CliParser.Parse(rootCommand, "-a cat -v carrot -a dog"); @@ -366,34 +366,34 @@ public void Parser_root_Options_can_be_specified_multiple_times_and_their_argume } /* - [Fact] - public void Options_can_be_specified_multiple_times_and_their_arguments_are_collated() - { - // TODO: tests AcceptOnlyFromAmong, fix - // TODO: This test does not appear to use AcceptOnlyFromAmong. Consider if test can just use normal strings - var animalsOption = new CliOption("-a", "--animals"); - animalsOption.AcceptOnlyFromAmong("dog", "cat", "sheep"); - var vegetablesOption = new CliOption("-v", "--vegetables"); - CliCommand command = - new CliCommand("the-command") { - animalsOption, - vegetablesOption - }; - - var result = command.Parse("the-command -a cat -v carrot -a dog"); - - result.GetResult(animalsOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("cat", "dog"); - - result.GetResult(vegetablesOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("carrot"); - } + [Fact] + public void Options_can_be_specified_multiple_times_and_their_arguments_are_collated() + { + // TODO: tests AcceptOnlyFromAmong, fix + // TODO: This test does not appear to use AcceptOnlyFromAmong. Consider if test can just use normal strings + var animalsOption = new CliOption("-a", "--animals"); + animalsOption.AcceptOnlyFromAmong("dog", "cat", "sheep"); + var vegetablesOption = new CliOption("-v", "--vegetables"); + CliCommand command = + new CliCommand("the-command") { + animalsOption, + vegetablesOption + }; + + var result = command.Parse("the-command -a cat -v carrot -a dog"); + + result.GetResult(animalsOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("cat", "dog"); + + result.GetResult(vegetablesOption) + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("carrot"); + } */ [Fact] @@ -406,55 +406,55 @@ public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_foll CliCommand command = new CliCommand("the-command") { - animalsOption, - vegetablesOption, - new CliArgument("arg") + animalsOption, + vegetablesOption, + new CliArgument("arg") }; var result = CliParser.Parse(command, "the-command -a cat some-arg -v carrot"); result.GetResult(animalsOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("cat"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("cat"); result.GetResult(vegetablesOption) - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("carrot"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("carrot"); result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("some-arg"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("some-arg"); } [Fact] public void Command_with_multiple_options_is_parsed_correctly() { var command = new CliCommand("outer") - { - new CliOption("--inner1"), - new CliOption("--inner2") - }; + { + new CliOption("--inner1"), + new CliOption("--inner2") + }; var result = CliParser.Parse(command, "outer --inner1 argument1 --inner2 argument2"); result.CommandResult - .Children - .Should() - .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner1" && - o.Tokens.Single().Value == "argument1"); + .Children + .Should() + .ContainSingle(o => + ((OptionResult)o).Option.Name == "--inner1" && + o.Tokens.Single().Value == "argument1"); result.CommandResult - .Children - .Should() - .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner2" && - o.Tokens.Single().Value == "argument2"); + .Children + .Should() + .ContainSingle(o => + ((OptionResult)o).Option.Name == "--inner2" && + o.Tokens.Single().Value == "argument2"); } [Fact(Skip = "Location means these are no longer equivalent.")] @@ -462,10 +462,10 @@ public void Command_with_multiple_options_is_parsed_correctly() public void Relative_order_of_arguments_and_options_within_a_command_does_not_matter() { var command = new CliCommand("move") - { - new CliArgument("arg"), - new CliOption("-X") - }; + { + new CliArgument("arg"), + new CliOption("-X") + }; // option before args ParseResult result1 = CliParser.Parse( @@ -484,17 +484,17 @@ public void Relative_order_of_arguments_and_options_within_a_command_does_not_ma // all should be equivalent result1.Should() - .BeEquivalentTo( - result2, - x => x.IgnoringCyclicReferences() - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); + .BeEquivalentTo( + result2, + x => x.IgnoringCyclicReferences() + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); result1.Should() - .BeEquivalentTo( - result3, - x => x.IgnoringCyclicReferences() - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); + .BeEquivalentTo( + result3, + x => x.IgnoringCyclicReferences() + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal)) + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); } [Theory] @@ -510,11 +510,11 @@ public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string c var rawSplit = CliParser.SplitCommandLine(commandLine); var command = new CliCommand("the-command") - { - new CliArgument("arg"), - new CliOption("--one"), - new CliOption("--many") - }; + { + new CliArgument("arg"), + new CliOption("--one"), + new CliOption("--many") + }; var result = CliParser.Parse(command, commandLine); @@ -522,96 +522,96 @@ public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string c } /* - [Fact] - public void An_outer_command_with_the_same_name_does_not_capture() + [Fact] + public void An_outer_command_with_the_same_name_does_not_capture() + { + // TODO: uses Diagram, fix + var command = new CliCommand("one") + { + new CliCommand("two") { - // TODO: uses Diagram, fix - var command = new CliCommand("one") - { - new CliCommand("two") - { - new CliCommand("three") - }, - new CliCommand("three") - }; - - ParseResult result = CliParser.Parse(command, "one two three"); - - result.Diagram().Should().Be("[ one [ two [ three ] ] ]"); - } + new CliCommand("three") + }, + new CliCommand("three") + }; + + ParseResult result = CliParser.Parse(command, "one two three"); - [Fact] - public void An_inner_command_with_the_same_name_does_not_capture() + result.Diagram().Should().Be("[ one [ two [ three ] ] ]"); + } + + [Fact] + public void An_inner_command_with_the_same_name_does_not_capture() + { + // TODO: uses Diagram, fix + var command = new CliCommand("one") + { + new CliCommand("two") { - // TODO: uses Diagram, fix - var command = new CliCommand("one") - { - new CliCommand("two") - { - new CliCommand("three") - }, - new CliCommand("three") - }; - - ParseResult result = CliParser.Parse(command, "one three"); - - result.Diagram().Should().Be("[ one [ three ] ]"); - } + new CliCommand("three") + }, + new CliCommand("three") + }; + + ParseResult result = CliParser.Parse(command, "one three"); + + result.Diagram().Should().Be("[ one [ three ] ]"); + } */ [Fact] public void When_nested_commands_all_accept_arguments_then_the_nearest_captures_the_arguments() { var command = new CliCommand("outer") - { - new CliArgument("arg1"), - new CliCommand("inner") - { - new CliArgument("arg2") - } - }; + { + new CliArgument("arg1"), + new CliCommand("inner") + { + new CliArgument("arg2") + } + }; var result = CliParser.Parse(command, "outer arg1 inner arg2"); result.CommandResult - .Parent - .Tokens.Select(t => t.Value) - .Should() - .BeEquivalentTo("arg1"); + .Parent + .Tokens.Select(t => t.Value) + .Should() + .BeEquivalentTo("arg1"); result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("arg2"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("arg2"); } /* - [Fact] - public void Nested_commands_with_colliding_names_cannot_both_be_applied() + [Fact] + public void Nested_commands_with_colliding_names_cannot_both_be_applied() + { + // TODO: uses Diagram, fix + var command = new CliCommand("outer") + { + new CliArgument("arg1"), + new CliCommand("non-unique") + { + new CliArgument("arg2") + }, + new CliCommand("inner") { - // TODO: uses Diagram, fix - var command = new CliCommand("outer") + new CliArgument("arg3"), + new CliCommand("non-unique") { - new CliArgument("arg1"), - new CliCommand("non-unique") - { - new CliArgument("arg2") - }, - new CliCommand("inner") - { - new CliArgument("arg3"), - new CliCommand("non-unique") - { - new CliArgument("arg4") - } - } - }; - - ParseResult result = command.Parse("outer arg1 inner arg2 non-unique arg3 "); - - result.Diagram().Should().Be("[ outer [ inner [ non-unique ] ] ]"); + new CliArgument("arg4") + } } + }; + + ParseResult result = command.Parse("outer arg1 inner arg2 non-unique arg3 "); + + result.Diagram().Should().Be("[ outer [ inner [ non-unique ] ] ]"); + } */ [Fact] @@ -619,10 +619,10 @@ public void When_child_option_will_not_accept_arg_then_parent_can() { var option = new CliOption("-x"); var command = new CliCommand("the-command") - { - option, - new CliArgument("arg") - }; + { + option, + new CliArgument("arg") + }; var result = CliParser.Parse(command, "the-command -x the-argument"); @@ -636,9 +636,9 @@ public void When_parent_option_will_not_accept_arg_then_child_can() { var option = new CliOption("-x"); var command = new CliCommand("the-command") - { - option - }; + { + option + }; var result = CliParser.Parse(command, "the-command -x the-argument"); @@ -652,10 +652,10 @@ public void Required_arguments_on_parent_commands_do_not_create_parse_errors_whe var child = new CliCommand("child"); var parent = new CliCommand("parent") - { - new CliArgument("arg"), - child - }; + { + new CliArgument("arg"), + child + }; var result = CliParser.Parse(parent, "child"); @@ -668,13 +668,13 @@ public void Required_arguments_on_grandparent_commands_do_not_create_parse_error var grandchild = new CliCommand("grandchild"); var grandparent = new CliCommand("grandparent") - { - new CliArgument("arg"), - new CliCommand("parent") - { - grandchild - } - }; + { + new CliArgument("arg"), + new CliCommand("parent") + { + grandchild + } + }; var result = CliParser.Parse(grandparent, "parent grandchild"); @@ -685,28 +685,28 @@ public void Required_arguments_on_grandparent_commands_do_not_create_parse_error public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_at_the_end_then_it_attaches_to_the_inner_command() { var outer = new CliCommand("outer") - { - new CliCommand("inner") - { - new CliOption("-x") - }, - new CliOption("-x") - }; + { + new CliCommand("inner") + { + new CliOption("-x") + }, + new CliOption("-x") + }; ParseResult result = CliParser.Parse(outer, "outer inner -x"); result.CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .AllBeAssignableTo(); + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .AllBeAssignableTo(); result.CommandResult - .Children - .Should() - .ContainSingle(o => ((OptionResult)o).Option.Name == "-x"); + .Children + .Should() + .ContainSingle(o => ((OptionResult)o).Option.Name == "-x"); } [Fact] @@ -721,50 +721,50 @@ public void When_options_with_the_same_name_are_defined_on_parent_and_child_comm var result = CliParser.Parse(outer, "outer -x inner"); result.CommandResult - .Children - .Should() - .BeEmpty(); + .Children + .Should() + .BeEmpty(); result.CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); } /* - [Fact] - // TODO: tests unmatched tokens, needs fix - public void Arguments_only_apply_to_the_nearest_command() + [Fact] + // TODO: tests unmatched tokens, needs fix + public void Arguments_only_apply_to_the_nearest_command() + { + var outer = new CliCommand("outer") + { + new CliArgument("arg1"), + new CliCommand("inner") { - var outer = new CliCommand("outer") - { - new CliArgument("arg1"), - new CliCommand("inner") - { - new CliArgument("arg2") - } - }; - - ParseResult result = outer.Parse("outer inner arg1 arg2"); - - result.CommandResult - .Parent - .Tokens - .Should() - .BeEmpty(); - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("arg1"); - result.UnmatchedTokens - .Should() - .BeEquivalentTo("arg2"); + new CliArgument("arg2") } + }; + + ParseResult result = outer.Parse("outer inner arg1 arg2"); + + result.CommandResult + .Parent + .Tokens + .Should() + .BeEmpty(); + result.CommandResult + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("arg1"); + result.UnmatchedTokens + .Should() + .BeEquivalentTo("arg2"); + } */ [Fact] @@ -774,40 +774,42 @@ public void Options_only_apply_to_the_nearest_command() var innerOption = new CliOption("-x"); var outer = new CliCommand("outer") - { - new CliCommand("inner") - { - innerOption - }, - outerOption - }; + { + new CliCommand("inner") + { + innerOption + }, + outerOption + }; var result = CliParser.Parse(outer, "outer inner -x one -x two"); result.RootCommandResult - .GetResult(outerOption) - .Should() - .BeNull(); + .GetResult(outerOption) + .Should() + .BeNull(); } [Fact] public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_as_arguments() { var command = new CliCommand("the-command") - { - new CliCommand("complete") - { - new CliArgument("arg"), - new CliOption("--position") - } - }; - - ParseResult result = CliParser.Parse(command, new[] { - "the-command", - "complete", - "--position", - "7", - "the-command" }); + { + new CliCommand("complete") + { + new CliArgument("arg"), + new CliOption("--position") + } + }; + + ParseResult result = CliParser.Parse( + command, new[] { + "the-command", + "complete", + "--position", + "7", + "the-command" + }); CommandResult completeResult = result.CommandResult; @@ -821,17 +823,17 @@ public void Absolute_unix_style_paths_are_lexed_correctly() @"rm ""/temp/the file.txt"""; CliCommand command = new("rm") - { - new CliArgument("arg") - }; + { + new CliArgument("arg") + }; var result = CliParser.Parse(command, commandText); result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .OnlyContain(a => a == @"/temp/the file.txt"); + .Tokens + .Select(t => t.Value) + .Should() + .OnlyContain(a => a == @"/temp/the file.txt"); } [Fact] @@ -841,20 +843,20 @@ public void Absolute_Windows_style_paths_are_lexed_correctly() @"rm ""c:\temp\the file.txt\"""; CliCommand command = new("rm") - { - new CliArgument("arg") - }; + { + new CliArgument("arg") + }; ParseResult result = CliParser.Parse(command, commandText); result.CommandResult - .Tokens - .Should() - .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); + .Tokens + .Should() + .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); } - // TODO: Defaults - /* +// TODO: Default values +/* [Fact] public void Commands_can_have_default_argument_values() { @@ -864,15 +866,15 @@ public void Commands_can_have_default_argument_values() }; var command = new CliCommand("command") - { - argument - }; + { + argument + }; ParseResult result = CliParser.Parse(command, "command"); GetValue(result, argument) - .Should() - .Be("default"); + .Should() + .Be("default"); } [Fact] @@ -900,16 +902,16 @@ public void When_an_option_with_a_default_value_is_not_matched_then_the_option_r }; var command = new CliCommand("command") - { - option - }; + { + option + }; var result = CliParser.Parse(command, "command"); result.GetResult(option) - .Implicit - .Should() - .BeTrue(); + .Implicit + .Should() + .BeTrue(); } [Fact] @@ -921,16 +923,16 @@ public void When_an_option_with_a_default_value_is_not_matched_then_there_are_no }; var command = new CliCommand("command") - { - option - }; + { + option + }; var result = CliParser.Parse(command, "command"); result.GetResult(option) - .IdentifierToken - .Should() - .BeEquivalentTo(default(CliToken)); + .IdentifierToken + .Should() + .BeEquivalentTo(default(CliToken)); } [Fact] @@ -942,15 +944,15 @@ public void When_an_argument_with_a_default_value_is_not_matched_then_there_are_ }; var command = new CliCommand("command") - { - argument - }; + { + argument + }; var result = CliParser.Parse(command, "command"); result.GetResult(argument) - .Tokens - .Should() - .BeEmpty(); + .Tokens + .Should() + .BeEmpty(); } [Fact] @@ -962,107 +964,107 @@ public void Command_default_argument_value_does_not_override_parsed_value() }; var command = new CliCommand("inner") - { - argument - }; + { + argument + }; var result = CliParser.Parse(command, "the-directory"); GetValue(result, argument) - .Name - .Should() - .Be("the-directory"); + .Name + .Should() + .Be("the-directory"); } - */ +*/ [Fact] public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_tokens() { var outer = new CliCommand("outer") + { + new CliCommand("inner") + { + new CliArgument("arg") { - new CliCommand("inner") - { - new CliArgument("arg") - { - Arity = ArgumentArity.OneOrMore - } - } - }; + Arity = ArgumentArity.OneOrMore + } + } + }; ParseResult result = CliParser.Parse(outer, "outer inner -p:RandomThing=random"); result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("-p:RandomThing=random"); + .Tokens + .Select(t => t.Value) + .Should() + .BeEquivalentTo("-p:RandomThing=random"); } /* - [Fact] - public void The_default_behavior_of_unmatched_tokens_resulting_in_errors_can_be_turned_off() - { - // TODO: uses UnmatchedTokens, TreatUnmatchedTokensAsErrors, fix - var command = new CliCommand("the-command") - { - new CliArgument("arg") - }; - command.TreatUnmatchedTokensAsErrors = false; + [Fact] + public void The_default_behavior_of_unmatched_tokens_resulting_in_errors_can_be_turned_off() + { + // TODO: uses UnmatchedTokens, TreatUnmatchedTokensAsErrors, fix + var command = new CliCommand("the-command") + { + new CliArgument("arg") + }; + command.TreatUnmatchedTokensAsErrors = false; - ParseResult result = command.Parse("the-command arg1 arg2"); + ParseResult result = command.Parse("the-command arg1 arg2"); - result.Errors.Should().BeEmpty(); + result.Errors.Should().BeEmpty(); - result.UnmatchedTokens - .Should() - .BeEquivalentTo("arg2"); - } + result.UnmatchedTokens + .Should() + .BeEquivalentTo("arg2"); + } */ [Fact] public void Option_and_Command_can_have_the_same_alias() { var innerCommand = new CliCommand("inner") - { - new CliArgument("arg1") - }; + { + new CliArgument("arg1") + }; var option = new CliOption("--inner"); var outerCommand = new CliCommand("outer") - { - innerCommand, - option, - new CliArgument("arg2") - }; + { + innerCommand, + option, + new CliArgument("arg2") + }; CliParser.Parse(outerCommand, "outer inner") - .CommandResult - .Command - .Should() - .BeSameAs(innerCommand); + .CommandResult + .Command + .Should() + .BeSameAs(innerCommand); CliParser.Parse(outerCommand, "outer --inner") - .CommandResult - .Command - .Should() - .BeSameAs(outerCommand); + .CommandResult + .Command + .Should() + .BeSameAs(outerCommand); CliParser.Parse(outerCommand, "outer --inner inner") - .CommandResult - .Command - .Should() - .BeSameAs(innerCommand); + .CommandResult + .Command + .Should() + .BeSameAs(innerCommand); CliParser.Parse(outerCommand, "outer --inner inner") - .CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .Contain(o => ((OptionResult)o).Option == option); + .CommandResult + .Parent + .Should() + .BeOfType() + .Which + .Children + .Should() + .Contain(o => ((OptionResult)o).Option == option); } [Fact] @@ -1072,21 +1074,21 @@ public void Options_can_have_the_same_alias_differentiated_only_by_prefix() var option2 = new CliOption("--a"); var rootCommand = new CliRootCommand - { - option1, - option2 - }; + { + option1, + option2 + }; CliParser.Parse(rootCommand, "-a").CommandResult - .Children - .Select(s => ((OptionResult)s).Option) - .Should() - .BeEquivalentTo(option1); + .Children + .Select(s => ((OptionResult)s).Option) + .Should() + .BeEquivalentTo(option1); CliParser.Parse(rootCommand, "--a").CommandResult - .Children - .Select(s => ((OptionResult)s).Option) - .Should() - .BeEquivalentTo(option2); + .Children + .Select(s => ((OptionResult)s).Option) + .Should() + .BeEquivalentTo(option2); } [Theory] @@ -1116,12 +1118,12 @@ public void When_an_option_argument_is_enclosed_in_double_quotes_its_value_retai public void Trailing_option_delimiters_are_ignored() { var rootCommand = new CliRootCommand - { - new CliCommand("subcommand") - { - new CliOption("--directory") - } - }; + { + new CliCommand("subcommand") + { + new CliOption("--directory") + } + }; var args = new[] { "subcommand", "--directory:", @"c:\" }; @@ -1130,9 +1132,9 @@ public void Trailing_option_delimiters_are_ignored() result.Errors.Should().BeEmpty(); result.Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentSequenceTo(new[] { "subcommand", "--directory", @"c:\" }); + .Select(t => t.Value) + .Should() + .BeEquivalentSequenceTo(new[] { "subcommand", "--directory", @"c:\" }); } [Theory] @@ -1144,10 +1146,10 @@ public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_op var optionX = new CliOption("-x"); var command = new CliCommand("command") - { - optionX, - new CliOption("-z") - }; + { + optionX, + new CliOption("-z") + }; var result = CliParser.Parse(command, input); @@ -1162,11 +1164,11 @@ public void Option_arguments_can_start_with_prefixes_that_make_them_look_like_bu var optionC = new CliOption("-c"); var command = new CliRootCommand - { - optionA, - optionB, - optionC - }; + { + optionA, + optionB, + optionC + }; var result = CliParser.Parse(command, "-a -bc"); @@ -1180,10 +1182,10 @@ public void Option_arguments_can_match_subcommands() { var optionA = new CliOption("-a"); var rootCommand = new CliRootCommand - { - new CliCommand("subcommand"), - optionA - }; + { + new CliCommand("subcommand"), + optionA + }; var result = CliParser.Parse(rootCommand, "-a subcommand"); @@ -1196,21 +1198,21 @@ public void Arguments_can_match_subcommands() { var argument = new CliArgument("arg"); var subcommand = new CliCommand("subcommand") - { - argument - }; + { + argument + }; var rootCommand = new CliRootCommand - { - subcommand - }; + { + subcommand + }; var result = CliParser.Parse(rootCommand, "subcommand one two three subcommand four"); result.CommandResult.Command.Should().BeSameAs(subcommand); GetValue(result, argument) - .Should() - .BeEquivalentSequenceTo("one", "two", "three", "subcommand", "four"); + .Should() + .BeEquivalentSequenceTo("one", "two", "three", "subcommand", "four"); } [Theory] @@ -1221,10 +1223,10 @@ public void Option_arguments_can_match_the_aliases_of_sibling_options_when_non_s var optionX = new CliOption("-x"); var command = new CliCommand("command") - { - optionX, - new CliOption("-y") - }; + { + optionX, + new CliOption("-y") + }; var result = CliParser.Parse(command, input); @@ -1238,9 +1240,9 @@ public void Single_option_arguments_that_match_option_aliases_are_parsed_correct var optionX = new CliOption("-x"); var command = new CliRootCommand - { - optionX - }; + { + optionX + }; var result = CliParser.Parse(command, "-x -x"); @@ -1262,10 +1264,10 @@ public void Boolean_options_are_not_greedy(string commandLine) var optY = new CliOption("-y"); var root = new CliRootCommand() - { - optX, - optY, - }; + { + optX, + optY, + }; var result = CliParser.Parse(root, commandLine); @@ -1282,10 +1284,10 @@ public void Multiple_option_arguments_that_match_multiple_arity_option_aliases_a var optionY = new CliOption("-y"); var command = new CliRootCommand - { - optionX, - optionY - }; + { + optionX, + optionY + }; var result = CliParser.Parse(command, "-x -x -x -y -y -x -y -y -y -x -x -y"); @@ -1300,10 +1302,10 @@ public void Bundled_option_arguments_that_match_option_aliases_are_parsed_correc var optionY = new CliOption("-y"); var command = new CliRootCommand - { - optionX, - optionY - }; + { + optionX, + optionY + }; var result = CliParser.Parse(command, "-yxx"); @@ -1317,10 +1319,10 @@ public void Argument_name_is_not_matched_as_a_token() var columnsArg = new CliArgument>("columns"); var command = new CliCommand("add") - { - nameArg, - columnsArg - }; + { + nameArg, + columnsArg + }; var result = CliParser.Parse(command, "name one two three"); @@ -1345,9 +1347,9 @@ public void Boolean_options_with_no_argument_specified_do_not_match_subsequent_a var option = new CliOption("-v"); var command = new CliCommand("command") - { - option - }; + { + option + }; var result = CliParser.Parse(command, "-v an-argument"); @@ -1355,80 +1357,80 @@ public void Boolean_options_with_no_argument_specified_do_not_match_subsequent_a } /* - [Fact] - public void When_a_command_line_has_unmatched_tokens_they_are_not_applied_to_subsequent_options() - { + [Fact] + public void When_a_command_line_has_unmatched_tokens_they_are_not_applied_to_subsequent_options() + { // TODO: uses TreatUnmatchedTokensAsErrors, fix - var command = new CliCommand("command") - { - TreatUnmatchedTokensAsErrors = false - }; - var optionX = new CliOption("-x"); - command.Options.Add(optionX); - var optionY = new CliOption("-y"); - command.Options.Add(optionY); - - var result = command.Parse("-x 23 unmatched-token -y 42"); - - GetValue(result, optionX).Should().Be("23"); - GetValue(result, optionY).Should().Be("42"); - result.UnmatchedTokens.Should().BeEquivalentTo("unmatched-token"); - } + var command = new CliCommand("command") + { + TreatUnmatchedTokensAsErrors = false + }; + var optionX = new CliOption("-x"); + command.Options.Add(optionX); + var optionY = new CliOption("-y"); + command.Options.Add(optionY); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void When_a_command_line_has_unmatched_tokens_the_parse_result_action_should_depend_on_parsed_command_TreatUnmatchedTokensAsErrors(bool treatUnmatchedTokensAsErrors) - { - // TODO: uses TreatUnmatchedTokensAsErrors, fix - CliRootCommand rootCommand = new(); - CliCommand subcommand = new("vstest") - { - new CliOption("--Platform"), - new CliOption("--Framework"), - new CliOption("--logger") - }; - subcommand.TreatUnmatchedTokensAsErrors = treatUnmatchedTokensAsErrors; - rootCommand.Subcommands.Add(subcommand); + var result = command.Parse("-x 23 unmatched-token -y 42"); - var result = rootCommand.Parse("vstest test1.dll test2.dll"); + GetValue(result, optionX).Should().Be("23"); + GetValue(result, optionY).Should().Be("42"); + result.UnmatchedTokens.Should().BeEquivalentTo("unmatched-token"); + } - result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void When_a_command_line_has_unmatched_tokens_the_parse_result_action_should_depend_on_parsed_command_TreatUnmatchedTokensAsErrors(bool treatUnmatchedTokensAsErrors) + { + // TODO: uses TreatUnmatchedTokensAsErrors, fix + CliRootCommand rootCommand = new(); + CliCommand subcommand = new("vstest") + { + new CliOption("--Platform"), + new CliOption("--Framework"), + new CliOption("--logger") + }; + subcommand.TreatUnmatchedTokensAsErrors = treatUnmatchedTokensAsErrors; + rootCommand.Subcommands.Add(subcommand); - if (treatUnmatchedTokensAsErrors) - { - result.Errors.Should().NotBeEmpty(); - result.Action.Should().NotBeSameAs(result.CommandResult.Command.Action); - } - else - { - result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); - } - } + var result = rootCommand.Parse("vstest test1.dll test2.dll"); - [Fact] - public void RootCommand_TreatUnmatchedTokensAsErrors_set_to_false_has_precedence_over_subcommands() - { - // TODO: uses TreatUnmatchedTokensAsErrors, fix - CliRootCommand rootCommand = new(); - rootCommand.TreatUnmatchedTokensAsErrors = false; - CliCommand subcommand = new("vstest") - { - new CliOption("--Platform"), - new CliOption("--Framework"), - new CliOption("--logger") - }; - subcommand.TreatUnmatchedTokensAsErrors = true; // the default, set to true to make it explicit - rootCommand.Subcommands.Add(subcommand); + result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); + + if (treatUnmatchedTokensAsErrors) + { + result.Errors.Should().NotBeEmpty(); + result.Action.Should().NotBeSameAs(result.CommandResult.Command.Action); + } + else + { + result.Errors.Should().BeEmpty(); + result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + } + } - var result = rootCommand.Parse("vstest test1.dll test2.dll"); + [Fact] + public void RootCommand_TreatUnmatchedTokensAsErrors_set_to_false_has_precedence_over_subcommands() + { + // TODO: uses TreatUnmatchedTokensAsErrors, fix + CliRootCommand rootCommand = new(); + rootCommand.TreatUnmatchedTokensAsErrors = false; + CliCommand subcommand = new("vstest") + { + new CliOption("--Platform"), + new CliOption("--Framework"), + new CliOption("--logger") + }; + subcommand.TreatUnmatchedTokensAsErrors = true; // the default, set to true to make it explicit + rootCommand.Subcommands.Add(subcommand); - result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); + var result = rootCommand.Parse("vstest test1.dll test2.dll"); - result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); - } + result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); + + result.Errors.Should().BeEmpty(); + result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + } */ [Fact] @@ -1447,18 +1449,18 @@ public void Command_argument_arity_can_be_a_fixed_value_greater_than_1() Arity = new ArgumentArity(3, 3) }; var command = new CliCommand("the-command") - { - argument - }; + { + argument + }; CliParser.Parse(command, "1 2 3") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument, dummyLocation), - new CliToken("2", CliTokenType.Argument, argument, dummyLocation), - new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument, dummyLocation), + new CliToken("2", CliTokenType.Argument, argument, dummyLocation), + new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); } [Fact] @@ -1469,67 +1471,67 @@ public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_tha Arity = new ArgumentArity(3, 5) }; var command = new CliCommand("the-command") - { - argument - }; + { + argument + }; CliParser.Parse(command, "1 2 3") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument, dummyLocation), - new CliToken("2", CliTokenType.Argument, argument, dummyLocation), - new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument, dummyLocation), + new CliToken("2", CliTokenType.Argument, argument, dummyLocation), + new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); CliParser.Parse(command, "1 2 3 4 5") - .CommandResult - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, argument, dummyLocation), - new CliToken("2", CliTokenType.Argument, argument, dummyLocation), - new CliToken("3", CliTokenType.Argument, argument, dummyLocation), - new CliToken("4", CliTokenType.Argument, argument, dummyLocation), - new CliToken("5", CliTokenType.Argument, argument, dummyLocation)); + .CommandResult + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, argument, dummyLocation), + new CliToken("2", CliTokenType.Argument, argument, dummyLocation), + new CliToken("3", CliTokenType.Argument, argument, dummyLocation), + new CliToken("4", CliTokenType.Argument, argument, dummyLocation), + new CliToken("5", CliTokenType.Argument, argument, dummyLocation)); } [Fact] public void When_command_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() { var command = new CliCommand("the-command") - { - new CliArgument("arg") - { - Arity = new ArgumentArity(2, 3) - } - }; + { + new CliArgument("arg") + { + Arity = new ArgumentArity(2, 3) + } + }; var result = CliParser.Parse(command, "1"); result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(command.Arguments[0]))); + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(command.Arguments[0]))); } [Fact] public void When_command_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() { var command = new CliCommand("the-command") - { - new CliArgument("arg") - { - Arity = new ArgumentArity(2, 3) - } - }; + { + new CliArgument("arg") + { + Arity = new ArgumentArity(2, 3) + } + }; ParseResult parseResult = CliParser.Parse(command, "1 2 3 4"); parseResult - .Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); + .Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); } [Fact] @@ -1538,18 +1540,18 @@ public void Option_argument_arity_can_be_a_fixed_value_greater_than_1() var option = new CliOption("-x") { Arity = new ArgumentArity(3, 3) }; var command = new CliCommand("the-command") - { - option - }; + { + option + }; CliParser.Parse(command, "-x 1 -x 2 -x 3") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default, dummyLocation), - new CliToken("2", CliTokenType.Argument, default, dummyLocation), - new CliToken("3", CliTokenType.Argument, default, dummyLocation)); + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default, dummyLocation), + new CliToken("2", CliTokenType.Argument, default, dummyLocation), + new CliToken("3", CliTokenType.Argument, default, dummyLocation)); } [Fact] @@ -1558,28 +1560,28 @@ public void Option_argument_arity_can_be_a_range_with_a_lower_bound_greater_than var option = new CliOption("-x") { Arity = new ArgumentArity(3, 5) }; var command = new CliCommand("the-command") - { - option - }; + { + option + }; CliParser.Parse(command, "-x 1 -x 2 -x 3") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default, dummyLocation), - new CliToken("2", CliTokenType.Argument, default, dummyLocation), - new CliToken("3", CliTokenType.Argument, default, dummyLocation)); + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default, dummyLocation), + new CliToken("2", CliTokenType.Argument, default, dummyLocation), + new CliToken("3", CliTokenType.Argument, default, dummyLocation)); CliParser.Parse(command, "-x 1 -x 2 -x 3 -x 4 -x 5") - .GetResult(option) - .Tokens - .Should() - .BeEquivalentTo( - new CliToken("1", CliTokenType.Argument, default, dummyLocation), - new CliToken("2", CliTokenType.Argument, default, dummyLocation), - new CliToken("3", CliTokenType.Argument, default, dummyLocation), - new CliToken("4", CliTokenType.Argument, default, dummyLocation), - new CliToken("5", CliTokenType.Argument, default, dummyLocation)); + .GetResult(option) + .Tokens + .Should() + .BeEquivalentTo( + new CliToken("1", CliTokenType.Argument, default, dummyLocation), + new CliToken("2", CliTokenType.Argument, default, dummyLocation), + new CliToken("3", CliTokenType.Argument, default, dummyLocation), + new CliToken("4", CliTokenType.Argument, default, dummyLocation), + new CliToken("5", CliTokenType.Argument, default, dummyLocation)); } [Fact] @@ -1591,31 +1593,31 @@ public void When_option_arguments_are_fewer_than_minimum_arity_then_an_error_is_ }; var command = new CliCommand("the-command") - { - option - }; + { + option + }; var result = CliParser.Parse(command, "-x 1"); result.Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(option))); + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.RequiredArgumentMissing(result.GetResult(option))); } [Fact] public void When_option_arguments_are_greater_than_maximum_arity_then_an_error_is_returned() { var command = new CliCommand("the-command") - { - new CliOption("-x") { Arity = new ArgumentArity(2, 3)} - }; + { + new CliOption("-x") { Arity = new ArgumentArity(2, 3)} + }; CliParser.Parse(command, "-x 1 2 3 4") - .Errors - .Select(e => e.Message) - .Should() - .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); + .Errors + .Select(e => e.Message) + .Should() + .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); } [Fact] @@ -1626,38 +1628,38 @@ public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_opti var result = CliParser.Parse(rootCommand, "jdbc url \"jdbc:sqlserver://10.0.0.2;databaseName=main\""); result.Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("url", - "jdbc:sqlserver://10.0.0.2;databaseName=main"); + .Select(t => t.Value) + .Should() + .BeEquivalentTo("url", + "jdbc:sqlserver://10.0.0.2;databaseName=main"); } /* - [Fact] - public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() - { - // TODO: uses GetCompletions, fix - // Tests bug identified in https://github.com/dotnet/command-line-api/issues/997 + [Fact] + public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() + { + // TODO: uses GetCompletions, fix + // Tests bug identified in https://github.com/dotnet/command-line-api/issues/997 - var argument1 = new CliArgument("arg1"); + var argument1 = new CliArgument("arg1"); - var argument2 = new CliArgument("arg2"); + var argument2 = new CliArgument("arg2"); - var command = new CliCommand("subcommand") - { - argument1, - argument2 - }; + var command = new CliCommand("subcommand") + { + argument1, + argument2 + }; - var rootCommand = new CliRootCommand - { - command - }; + var rootCommand = new CliRootCommand + { + command + }; - var parseResult = rootCommand.Parse("subcommand arg1 arg2"); + var parseResult = rootCommand.Parse("subcommand arg1 arg2"); - Action act = () => parseResult.GetCompletions(); - act.Should().NotThrow(); - } + Action act = () => parseResult.GetCompletions(); + act.Should().NotThrow(); + } */ [Theory] // https://github.com/dotnet/command-line-api/issues/1551, https://github.com/dotnet/command-line-api/issues/1533 @@ -1672,9 +1674,9 @@ public void Parsed_value_of_empty_string_arg_is_an_empty_string(string arg1, str }; var rootCommand = new CliRootCommand - { - option - }; + { + option + }; var result = CliParser.Parse(rootCommand, new[] { arg1, arg2 }); @@ -1687,21 +1689,18 @@ public void CommandResult_contains_argument_ValueResults() { var argument1 = new CliArgument("arg1"); var argument2 = new CliArgument("arg2"); - var command = new CliCommand("subcommand") - { - argument1, - argument2 - }; - + { + argument1, + argument2 + }; var rootCommand = new CliRootCommand - { - command - }; + { + command + }; var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandResult = parseResult.CommandResult; commandResult.ValueResults.Should().HaveCount(2); var result1 = commandResult.ValueResults[0]; @@ -1715,21 +1714,18 @@ public void CommandResult_contains_option_ValueResults() { var argument1 = new CliOption("--opt1"); var argument2 = new CliOption("--opt2"); - var command = new CliCommand("subcommand") - { - argument1, - argument2 - }; - + { + argument1, + argument2 + }; var rootCommand = new CliRootCommand - { - command - }; + { + command + }; var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); - var commandResult = parseResult.CommandResult; commandResult.ValueResults.Should().HaveCount(2); var result1 = commandResult.ValueResults[0]; @@ -1743,24 +1739,21 @@ public void Location_in_ValueResult_correct_for_arguments() { var argument1 = new CliArgument("arg1"); var argument2 = new CliArgument("arg2"); - var command = new CliCommand("subcommand") - { - argument1, - argument2 - }; - + { + argument1, + argument2 + }; var rootCommand = new CliRootCommand - { - command - }; + { + command + }; var expectedOuterLocation = new Location("testhost", Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 1, expectedOuterLocation); var expectedLocation2 = new Location("Spock", Location.User, 2, expectedOuterLocation); var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandResult = parseResult.CommandResult; var result1 = commandResult.ValueResults[0]; var result2 = commandResult.ValueResults[1]; @@ -1773,17 +1766,15 @@ public void Location_in_ValueResult_correct_for_options() { var option1 = new CliOption("--opt1"); var option2 = new CliOption("--opt2"); - var command = new CliCommand("subcommand") - { - option1, - option2 - }; - + { + option1, + option2 + }; var rootCommand = new CliRootCommand - { - command - }; + { + command + }; var expectedOuterLocation = new Location("testhost", Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 3, expectedOuterLocation); var expectedLocation2 = new Location("Spock", Location.User, 5, expectedOuterLocation); @@ -1803,14 +1794,14 @@ public void Location_offsets_in_ValueResult_correct_for_arguments() var argument1 = new CliArgument("arg1"); var command = new CliCommand("subcommand") - { - argument1, - }; + { + argument1, + }; var rootCommand = new CliRootCommand - { - command - }; + { + command + }; var expectedOuterLocation = new Location("testhost", Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 1, expectedOuterLocation); var expectedLocation2 = new Location("Spock", Location.User, 2, expectedOuterLocation); @@ -1827,16 +1818,14 @@ public void Location_offsets_in_ValueResult_correct_for_arguments() public void Location_offsets_in_ValueResult_correct_for_options() { var option1 = new CliOption("--opt1"); - var command = new CliCommand("subcommand") - { - option1, - }; - + { + option1, + }; var rootCommand = new CliRootCommand - { - command - }; + { + command + }; var expectedOuterLocation = new Location("testhost", Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 3, expectedOuterLocation); var expectedLocation2 = new Location("Spock", Location.User, 5, expectedOuterLocation); @@ -1854,17 +1843,15 @@ public void Location_offset_correct_when_colon_or_equal_used() { var option1 = new CliOption("--opt1"); var option2 = new CliOption("--opt11"); - var command = new CliCommand("subcommand") - { - option1, - option2 - }; - + { + option1, + option2 + }; var rootCommand = new CliRootCommand - { - command - }; + { + command + }; var expectedOuterLocation = new Location("testhost", Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 2, expectedOuterLocation, 7); var expectedLocation2 = new Location("Spock", Location.User, 3, expectedOuterLocation, 8); @@ -1883,17 +1870,15 @@ public void ParseResult_contains_argument_ValueResults() { var argument1 = new CliArgument("arg1"); var argument2 = new CliArgument("arg2"); - var command = new CliCommand("subcommand") - { - argument1, - argument2 - }; - + { + argument1, + argument2 + }; var rootCommand = new CliRootCommand - { - command - }; + { + command + }; var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); @@ -1909,17 +1894,15 @@ public void ParseResult_contains_option_ValueResults() { var option1 = new CliOption("--opt1"); var option2 = new CliOption("--opt2"); - var command = new CliCommand("subcommand") - { - option1, - option2 - }; - + { + option1, + option2 + }; var rootCommand = new CliRootCommand - { - command - }; + { + command + }; var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); @@ -1929,11 +1912,5 @@ public void ParseResult_contains_option_ValueResults() result1.GetValue().Should().Be("Kirk"); result2.GetValue().Should().Be("Spock"); } - - [Fact] - public void Value_result_returned_in_simple_case() - { - - } } } diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index f395cbca81..4c6629312b 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -17,31 +17,31 @@ public sealed class ParseResult private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); private readonly CommandResult _rootCommandResult; - // TODO: unmatched tokens, invocation, completion - /* - private readonly IReadOnlyList _unmatchedTokens; - private CompletionContext? _completionContext; - private readonly CliAction? _action; - private readonly List? _preActions; - */ +// TODO: unmatched tokens, invocation, completion +/* + private readonly IReadOnlyList _unmatchedTokens; + private CompletionContext? _completionContext; + private readonly CliAction? _action; + private readonly List? _preActions; +*/ internal ParseResult( CliConfiguration configuration, - // TODO: determine how rootCommandResult and commandResult differ +// TODO: determine how rootCommandResult and commandResult differ CommandResult rootCommandResult, CommandResult commandResult, Dictionary valueResults, List tokens, - // TODO: unmatched tokens - // List? unmatchedTokens, +// TODO: unmatched tokens +// List? unmatchedTokens, List? errors, - // TODO: commandLineText should be string array +// TODO: commandLineText should be string array string? commandLineText = null //, - // TODO: invocation - /* - CliAction? action = null, - List? preActions = null) - */ +// TODO: invocation +/* + CliAction? action = null, + List? preActions = null) +*/ ) { Configuration = configuration; @@ -49,10 +49,10 @@ internal ParseResult( CommandResult = commandResult; valueResultDictionary = valueResults; // TODO: invocation - /* - _action = action; - _preActions = preActions; - */ +/* + _action = action; + _preActions = preActions; +*/ // skip the root command when populating Tokens property if (tokens.Count > 1) @@ -70,16 +70,16 @@ internal ParseResult( CommandLineText = commandLineText; - // TODO: unmatched tokens - // _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; +// TODO: unmatched tokens +// _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; Errors = errors is not null ? errors : Array.Empty(); } - // TODO: check that constructing empty ParseResult directly is correct - /* - internal static ParseResult Empty() => new CliRootCommand().Parse(Array.Empty()); - */ +// TODO: check that constructing empty ParseResult directly is correct +/* + internal static ParseResult Empty() => new CliRootCommand().Parse(Array.Empty()); +*/ /// /// A result indicating the command specified in the command line input. @@ -194,15 +194,15 @@ CommandLineText is null public OptionResult? GetResult(CliOption option) => _rootCommandResult.GetResult(option); - // TODO: Directives - /* - /// - /// Gets the result, if any, for the specified directive. - /// - /// The directive for which to find a result. - /// A result for the specified directive, or if it was not provided. - public DirectiveResult? GetResult(CliDirective directive) => _rootCommandResult.GetResult(directive); - */ +// TODO: Directives +/* + /// + /// Gets the result, if any, for the specified directive. + /// + /// The directive for which to find a result. + /// A result for the specified directive, or if it was not provided. + public DirectiveResult? GetResult(CliDirective directive) => _rootCommandResult.GetResult(directive); +*/ /// /// Gets the result, if any, for the specified symbol. /// @@ -211,169 +211,169 @@ CommandLineText is null public SymbolResult? GetResult(CliSymbol symbol) => _rootCommandResult.SymbolResultTree.TryGetValue(symbol, out SymbolResult? result) ? result : null; - // TODO: completion, invocation - /* - /// - /// Gets completions based on a given parse result. - /// - /// The position at which completions are requested. - /// A set of completions for completion. - public IEnumerable GetCompletions( - int? position = null) - { - SymbolResult currentSymbolResult = SymbolToComplete(position); +// TODO: completion, invocation +/* + /// + /// Gets completions based on a given parse result. + /// + /// The position at which completions are requested. + /// A set of completions for completion. + public IEnumerable GetCompletions( + int? position = null) + { + SymbolResult currentSymbolResult = SymbolToComplete(position); - CliSymbol currentSymbol = currentSymbolResult switch - { - ArgumentResult argumentResult => argumentResult.Argument, - OptionResult optionResult => optionResult.Option, - DirectiveResult directiveResult => directiveResult.Directive, - _ => ((CommandResult)currentSymbolResult).Command - }; + CliSymbol currentSymbol = currentSymbolResult switch + { + ArgumentResult argumentResult => argumentResult.Argument, + OptionResult optionResult => optionResult.Option, + DirectiveResult directiveResult => directiveResult.Directive, + _ => ((CommandResult)currentSymbolResult).Command + }; - var context = GetCompletionContext(); + var context = GetCompletionContext(); - if (position is not null && - context is TextCompletionContext tcc) - { - context = tcc.AtCursorPosition(position.Value); - } + if (position is not null && + context is TextCompletionContext tcc) + { + context = tcc.AtCursorPosition(position.Value); + } - var completions = currentSymbol.GetCompletions(context); + var completions = currentSymbol.GetCompletions(context); - string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult - ? OptionsWithArgumentLimitReached(commandResult) - : Array.Empty(); + string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult + ? OptionsWithArgumentLimitReached(commandResult) + : Array.Empty(); - completions = - completions.Where(item => optionsWithArgumentLimitReached.All(s => s != item.Label)); + completions = + completions.Where(item => optionsWithArgumentLimitReached.All(s => s != item.Label)); - return completions; + return completions; - static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => - commandResult - .Children - .OfType() - .Where(c => c.IsArgumentLimitReached) - .Select(o => o.Option) - .SelectMany(c => new[] { c.Name }.Concat(c.Aliases)) - .ToArray(); - } + static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => + commandResult + .Children + .OfType() + .Where(c => c.IsArgumentLimitReached) + .Select(o => o.Option) + .SelectMany(c => new[] { c.Name }.Concat(c.Aliases)) + .ToArray(); + } - /// - /// Invokes the appropriate command handler for a parsed command line input. - /// - /// A token that can be used to cancel an invocation. - /// A task whose result can be used as a process exit code. - public Task InvokeAsync(CancellationToken cancellationToken = default) - => InvocationPipeline.InvokeAsync(this, cancellationToken); - - /// - /// Invokes the appropriate command handler for a parsed command line input. - /// - /// A value that can be used as a process exit code. - public int Invoke() - { - var useAsync = false; + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// A token that can be used to cancel an invocation. + /// A task whose result can be used as a process exit code. + public Task InvokeAsync(CancellationToken cancellationToken = default) + => InvocationPipeline.InvokeAsync(this, cancellationToken); - if (Action is AsynchronousCliAction) - { - useAsync = true; - } - else if (PreActions is not null) - { - for (var i = 0; i < PreActions.Count; i++) - { - var action = PreActions[i]; - if (action is AsynchronousCliAction) - { - useAsync = true; - break; - } - } - } + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// A value that can be used as a process exit code. + public int Invoke() + { + var useAsync = false; - if (useAsync) - { - return InvocationPipeline.InvokeAsync(this, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); - } - else + if (Action is AsynchronousCliAction) + { + useAsync = true; + } + else if (PreActions is not null) + { + for (var i = 0; i < PreActions.Count; i++) + { + var action = PreActions[i]; + if (action is AsynchronousCliAction) { - return InvocationPipeline.Invoke(this); + useAsync = true; + break; } } + } + + if (useAsync) + { + return InvocationPipeline.InvokeAsync(this, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + } + else + { + return InvocationPipeline.Invoke(this); + } + } - /// - /// Gets the for parsed result. The handler represents the action - /// that will be performed when the parse result is invoked. - /// - public CliAction? Action => _action ?? CommandResult.Command.Action; + /// + /// Gets the for parsed result. The handler represents the action + /// that will be performed when the parse result is invoked. + /// + public CliAction? Action => _action ?? CommandResult.Command.Action; - internal IReadOnlyList? PreActions => _preActions; + internal IReadOnlyList? PreActions => _preActions; - private SymbolResult SymbolToComplete(int? position = null) - { - var commandResult = CommandResult; + private SymbolResult SymbolToComplete(int? position = null) + { + var commandResult = CommandResult; - var allSymbolResultsForCompletion = AllSymbolResultsForCompletion(); + var allSymbolResultsForCompletion = AllSymbolResultsForCompletion(); - var currentSymbol = allSymbolResultsForCompletion.Last(); + var currentSymbol = allSymbolResultsForCompletion.Last(); - return currentSymbol; + return currentSymbol; - IEnumerable AllSymbolResultsForCompletion() + IEnumerable AllSymbolResultsForCompletion() + { + foreach (var item in commandResult.AllSymbolResults()) + { + if (item is CommandResult command) { - foreach (var item in commandResult.AllSymbolResults()) - { - if (item is CommandResult command) - { - yield return command; - } - else if (item is OptionResult option) - { - if (WillAcceptAnArgument(this, position, option)) - { - yield return option; - } - } - } + yield return command; } - - static bool WillAcceptAnArgument( - ParseResult parseResult, - int? position, - OptionResult optionResult) + else if (item is OptionResult option) { - if (optionResult.Implicit) + if (WillAcceptAnArgument(this, position, option)) { - return false; + yield return option; } + } + } + } - if (!optionResult.IsArgumentLimitReached) - { - return true; - } + static bool WillAcceptAnArgument( + ParseResult parseResult, + int? position, + OptionResult optionResult) + { + if (optionResult.Implicit) + { + return false; + } - var completionContext = parseResult.GetCompletionContext(); + if (!optionResult.IsArgumentLimitReached) + { + return true; + } - if (completionContext is TextCompletionContext textCompletionContext) - { - if (position.HasValue) - { - textCompletionContext = textCompletionContext.AtCursorPosition(position.Value); - } + var completionContext = parseResult.GetCompletionContext(); - if (textCompletionContext.WordToComplete.Length > 0) - { - var tokenToComplete = parseResult.Tokens.Last(t => t.Value == textCompletionContext.WordToComplete); + if (completionContext is TextCompletionContext textCompletionContext) + { + if (position.HasValue) + { + textCompletionContext = textCompletionContext.AtCursorPosition(position.Value); + } - return optionResult.Tokens.Contains(tokenToComplete); - } - } + if (textCompletionContext.WordToComplete.Length > 0) + { + var tokenToComplete = parseResult.Tokens.Last(t => t.Value == textCompletionContext.WordToComplete); - return !optionResult.IsArgumentLimitReached; + return optionResult.Tokens.Contains(tokenToComplete); } } - */ + + return !optionResult.IsArgumentLimitReached; + } + } + */ } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/ArgumentResult.cs index 133545134a..1a7c6e4cd7 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/ArgumentResult.cs @@ -151,26 +151,26 @@ private ArgumentConversionResult ValidateAndConvert(bool useValidators) { return ReportErrorIfNeeded(arityFailure); } - // TODO: validators - /* - // 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) - { - for (var i = 0; i < Argument.Validators.Count; i++) - { - Argument.Validators[i](this); - } - - // validator provided by the user might report an error, which sets _conversionResult - if (_conversionResult is not null) - { - return _conversionResult; - } - } - */ +// TODO: validators +/* + // 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) + { + for (var i = 0; i < Argument.Validators.Count; i++) + { + Argument.Validators[i](this); + } + + // validator provided by the user might report an error, which sets _conversionResult + if (_conversionResult is not null) + { + return _conversionResult; + } + } +*/ // TODO: defaults /* diff --git a/src/System.CommandLine/Parsing/CliToken.cs b/src/System.CommandLine/Parsing/CliToken.cs index 5ee2bea4e8..e10ab3a986 100644 --- a/src/System.CommandLine/Parsing/CliToken.cs +++ b/src/System.CommandLine/Parsing/CliToken.cs @@ -19,7 +19,6 @@ public static CliToken CreateFromOtherToken(CliToken otherToken, string? arg, Lo /// The string value of the token. /// The type of the token. /// The symbol represented by the token - public CliToken(string? value, CliTokenType type, CliSymbol symbol) { Value = value ?? ""; diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CommandResult.cs index a1802f7c1c..7adf32d08f 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CommandResult.cs @@ -174,7 +174,7 @@ private void ValidateOptions(bool completeValidation) } // TODO: Validation - private void ValidateArguments(bool completeValidation) + private void ValidateArguments(bool completeValidation) { var arguments = Command.Arguments; for (var i = 0; i < arguments.Count; i++) diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 48e0852253..a751537185 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -89,7 +89,6 @@ internal ParseResult Parse() _tokens, // TODO: unmatched tokens // _symbolResultTree.UnmatchedTokens, - _symbolResultTree.Errors, _rawInput // TODO: invocation diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 5c38e21479..9927aa4dd9 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -187,7 +187,7 @@ internal static void Tokenize( return errors; } - + static bool TryGetSymbolAndTokenType(Dictionary validTokens, string arg, [NotNullWhen(true)] out CliSymbol? symbol, diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index f213181ba0..37319d604a 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -10,10 +10,10 @@ internal sealed class SymbolResultTree : Dictionary { private readonly CliCommand _rootCommand; internal List? Errors; - // TODO: unmatched tokens - /* - internal List? UnmatchedTokens; - */ +// TODO: unmatched tokens +/* + internal List? UnmatchedTokens; +*/ // TODO: Looks like this is a SymboNode/linked list because a symbol may appear multiple // places in the tree and multiple symbols will have the same short name. The question is @@ -46,11 +46,11 @@ internal SymbolResultTree( internal OptionResult? GetResult(CliOption option) => TryGetValue(option, out SymbolResult? result) ? (OptionResult)result : default; - //TODO: directives - /* - internal DirectiveResult? GetResult(CliDirective directive) - => TryGetValue(directive, out SymbolResult? result) ? (DirectiveResult)result : default; - */ +//TODO: directives +/* + internal DirectiveResult? GetResult(CliDirective directive) + => TryGetValue(directive, out SymbolResult? result) ? (DirectiveResult)result : default; +*/ // TODO: Determine how this is used. It appears to be O^n in the size of the tree and so if it is called multiple times, we should reconsider to avoid O^(N*M) internal IEnumerable GetChildren(SymbolResult parent) { @@ -92,18 +92,18 @@ internal Dictionary GetValueResultDictionary() internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, CommandResult rootCommandResult) { - /* - // TODO: unmatched tokens - (UnmatchedTokens ??= new()).Add(token); - - if (commandResult.Command.TreatUnmatchedTokensAsErrors) - { - if (commandResult != rootCommandResult && !rootCommandResult.Command.TreatUnmatchedTokensAsErrors) - { - return; - } - - */ +/* +// TODO: unmatched tokens + (UnmatchedTokens ??= new()).Add(token); + + if (commandResult.Command.TreatUnmatchedTokensAsErrors) + { + if (commandResult != rootCommandResult && !rootCommandResult.Command.TreatUnmatchedTokensAsErrors) + { + return; + } + +*/ AddError(new ParseError(LocalizationResources.UnrecognizedCommandOrArgument(token.Value), commandResult)); // } } @@ -135,12 +135,12 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com return null; } - // TODO: symbolsbyname - this is inefficient - // results for some values may not be queried at all, dependent on other options - // so we could avoid using their value factories and adding them to the dictionary - // could we sort by name allowing us to do a binary search instead of allocating a dictionary? - // could we add codepaths that query for specific kinds of symbols so they don't have to search all symbols? - // Additional Note: Couldn't commands know their children, and thus this involves querying the active command, and possibly the parents +// TODO: symbolsbyname - this is inefficient +// results for some values may not be queried at all, dependent on other options +// so we could avoid using their value factories and adding them to the dictionary +// could we sort by name allowing us to do a binary search instead of allocating a dictionary? +// could we add codepaths that query for specific kinds of symbols so they don't have to search all symbols? +// Additional Note: Couldn't commands know their children, and thus this involves querying the active command, and possibly the parents private void PopulateSymbolsByName(CliCommand command) { if (command.HasArguments) diff --git a/src/System.CommandLine/Parsing/ValueResultOutcome.cs b/src/System.CommandLine/Parsing/ValueResultOutcome.cs index f4a465a59b..ccfc8d70b0 100644 --- a/src/System.CommandLine/Parsing/ValueResultOutcome.cs +++ b/src/System.CommandLine/Parsing/ValueResultOutcome.cs @@ -8,4 +8,4 @@ public enum ValueResultOutcome NoArgument, // NoArgumentConversionResult Success, // SuccessfulArgumentConversionResult HasErrors, // FailedArgumentConversionResult, there are one or more errors -} \ No newline at end of file +} From 9ae597c1d95c30d51e96839adbb1a9dfc2144006 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Tue, 16 Apr 2024 21:12:56 -0400 Subject: [PATCH 051/150] Fixed tests to use exe name, rather than hardcoding testhost The exe name appears to differ in CI --- src/System.CommandLine.Tests/ParserTests.cs | 10 +++++----- src/System.CommandLine.Tests/TokenizerTests.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 8ef106c8a3..1a079ad78b 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -1748,7 +1748,7 @@ public void Location_in_ValueResult_correct_for_arguments() { command }; - var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedOuterLocation = new Location(CliExecutable.ExecutableName, Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 1, expectedOuterLocation); var expectedLocation2 = new Location("Spock", Location.User, 2, expectedOuterLocation); @@ -1775,7 +1775,7 @@ public void Location_in_ValueResult_correct_for_options() { command }; - var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedOuterLocation = new Location(CliExecutable.ExecutableName, Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 3, expectedOuterLocation); var expectedLocation2 = new Location("Spock", Location.User, 5, expectedOuterLocation); @@ -1802,7 +1802,7 @@ public void Location_offsets_in_ValueResult_correct_for_arguments() { command }; - var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedOuterLocation = new Location(CliExecutable.ExecutableName, Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 1, expectedOuterLocation); var expectedLocation2 = new Location("Spock", Location.User, 2, expectedOuterLocation); @@ -1826,7 +1826,7 @@ public void Location_offsets_in_ValueResult_correct_for_options() { command }; - var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedOuterLocation = new Location(CliExecutable.ExecutableName, Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 3, expectedOuterLocation); var expectedLocation2 = new Location("Spock", Location.User, 5, expectedOuterLocation); @@ -1852,7 +1852,7 @@ public void Location_offset_correct_when_colon_or_equal_used() { command }; - var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedOuterLocation = new Location(CliExecutable.ExecutableName, Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 2, expectedOuterLocation, 7); var expectedLocation2 = new Location("Spock", Location.User, 3, expectedOuterLocation, 8); diff --git a/src/System.CommandLine.Tests/TokenizerTests.cs b/src/System.CommandLine.Tests/TokenizerTests.cs index 0184e990bf..f55922e511 100644 --- a/src/System.CommandLine.Tests/TokenizerTests.cs +++ b/src/System.CommandLine.Tests/TokenizerTests.cs @@ -56,8 +56,8 @@ public void Location_stack_ToString_is_correct() errors.Should().BeNull(); tokens.Count.Should().Be(3); locations.Count.Should().Be(2); - locations[0].Should().Be("testhost from User[-1, 8, 0]; --hello from User[0, 7, 0]"); - locations[1].Should().Be("testhost from User[-1, 8, 0]; world from User[1, 5, 0]"); + locations[0].Should().Be($"{CliExecutable.ExecutableName} from User[-1, {CliExecutable.ExecutableName.Length}, 0]; --hello from User[0, 7, 0]"); + locations[1].Should().Be($"{CliExecutable.ExecutableName} from User[-1, {CliExecutable.ExecutableName.Length}, 0]; world from User[1, 5, 0]"); } [Fact] From 85b4dbeb072296977b412a182f47a414c80995db Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Fri, 19 Apr 2024 20:21:02 -0400 Subject: [PATCH 052/150] Removed Tokens from ParseResult --- src/System.CommandLine.Tests/ParserTests.cs | 9 +++++++++ src/System.CommandLine/ParseResult.cs | 7 ++++++- src/System.CommandLine/Parsing/ParseOperation.cs | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 1a079ad78b..72280dd9c0 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -497,6 +497,8 @@ public void Relative_order_of_arguments_and_options_within_a_command_does_not_ma .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); } + // TODO: Tests tokens which is no longer exposed, and should be replaced by tests of location or removed + /* [Theory] [InlineData("--one 1 --many 1 --many 2")] [InlineData("--one 1 --many 1 --many 2 arg1 arg2")] @@ -520,6 +522,7 @@ public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string c result.Tokens.Select(t => t.Value).Should().Equal(rawSplit); } + */ /* [Fact] @@ -1114,6 +1117,8 @@ public void When_an_option_argument_is_enclosed_in_double_quotes_its_value_retai .BeEquivalentTo(new[] { arg2 }); } + // TODO: Tests tokens which is no longer exposed, and should be replaced by tests of location or removed + /* [Fact] // https://github.com/dotnet/command-line-api/issues/1445 public void Trailing_option_delimiters_are_ignored() { @@ -1136,6 +1141,7 @@ public void Trailing_option_delimiters_are_ignored() .Should() .BeEquivalentSequenceTo(new[] { "subcommand", "--directory", @"c:\" }); } + */ [Theory] [InlineData("-x -y")] @@ -1620,6 +1626,8 @@ public void When_option_arguments_are_greater_than_maximum_arity_then_an_error_i .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); } + // TODO: Tests tokens which is no longer exposed, and should be replaced with equivalent test using ParseResult + /* [Fact] public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_option() { @@ -1633,6 +1641,7 @@ public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_opti .BeEquivalentTo("url", "jdbc:sqlserver://10.0.0.2;databaseName=main"); } + */ /* [Fact] public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 4c6629312b..c0001edb14 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -31,7 +31,9 @@ internal ParseResult( CommandResult rootCommandResult, CommandResult commandResult, Dictionary valueResults, + /* List tokens, + */ // TODO: unmatched tokens // List? unmatchedTokens, List? errors, @@ -53,7 +55,7 @@ internal ParseResult( _action = action; _preActions = preActions; */ - + /* // skip the root command when populating Tokens property if (tokens.Count > 1) { @@ -67,6 +69,7 @@ internal ParseResult( { Tokens = Array.Empty(); } + */ CommandLineText = commandLineText; @@ -101,12 +104,14 @@ internal ParseResult( /// public IReadOnlyList Errors { get; } + /* // TODO: don't expose tokens // TODO: This appears to be set, but only read during testing. Consider removing. /// /// Gets the tokens identified while parsing command line input. /// internal IReadOnlyList Tokens { get; } + */ // TODO: This appears to be set, but never used. Consider removing. /// diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index a751537185..d78a0e55e8 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -86,7 +86,9 @@ internal ParseResult Parse() _rootCommandResult, _innermostCommandResult, _rootCommandResult.SymbolResultTree.GetValueResultDictionary(), + /* _tokens, + */ // TODO: unmatched tokens // _symbolResultTree.UnmatchedTokens, _symbolResultTree.Errors, From aaea93a4ef77524dc3582c68123b505bab53e9dd Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 20 Apr 2024 10:22:32 -0400 Subject: [PATCH 053/150] Remove suppresion to fix #1978 --- .../Binding/ArgumentConverter.DefaultValues.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs b/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs index a44a8114d9..f921b0603e 100644 --- a/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs +++ b/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs @@ -14,7 +14,6 @@ internal static partial class ArgumentConverter private static ConstructorInfo? _listCtor; #endif - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "https://github.com/dotnet/command-line-api/issues/1638")] private static Array CreateArray(Type itemType, int capacity) => Array.CreateInstance(itemType, capacity); From 5543d779c96ad9a458761923a9740daf2cd835dd Mon Sep 17 00:00:00 2001 From: Jean Joeris Date: Sat, 23 Mar 2024 14:21:14 -0400 Subject: [PATCH 054/150] Add basic logic to ErrorReportingSubsystem --- .../ConsoleHack.cs | 2 +- .../ErrorReportingSubsystem.cs | 28 +++++++++++++++++-- src/System.CommandLine/ConsoleHelpers.cs | 7 +++-- .../System.CommandLine.csproj | 1 + 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/System.CommandLine.Subsystems/ConsoleHack.cs b/src/System.CommandLine.Subsystems/ConsoleHack.cs index 78548b6b4f..02f8a6bb2b 100644 --- a/src/System.CommandLine.Subsystems/ConsoleHack.cs +++ b/src/System.CommandLine.Subsystems/ConsoleHack.cs @@ -10,7 +10,7 @@ public class ConsoleHack private readonly StringBuilder buffer = new(); private bool redirecting = false; - public void WriteLine(string text) + public void WriteLine(string text = "") { if (redirecting) { diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs index bb14fddb1d..7104f2781b 100644 --- a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -1,24 +1,48 @@ // 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.Parsing; using System.CommandLine.Subsystems; using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine; +/// +/// +/// +/// +/// public class ErrorReportingSubsystem : CliSubsystem { public ErrorReportingSubsystem(IAnnotationProvider? annotationProvider = null) : base(ErrorReportingAnnotations.Prefix, SubsystemKind.ErrorReporting, annotationProvider) { } - // TODO: Stash option rather than using string protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.Errors.Any(); protected internal override CliExit Execute(PipelineContext pipelineContext) { - pipelineContext.ConsoleHack.WriteLine("You have errors!"); + var _ = pipelineContext.ParseResult + ?? throw new ArgumentNullException($"{nameof(pipelineContext)}.ParseResult"); + + Report(pipelineContext.ConsoleHack, pipelineContext.ParseResult.Errors); + return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); } + + // TODO: internal, protected virtual? + public void Report(ConsoleHack consoleHack, IReadOnlyList errors) + { + ConsoleHelpers.ResetTerminalForegroundColor(); + ConsoleHelpers.SetTerminalForegroundRed(); + + foreach (var error in errors) + { + consoleHack.WriteLine(error.Message); + } + consoleHack.WriteLine(); + + ConsoleHelpers.ResetTerminalForegroundColor(); + } } diff --git a/src/System.CommandLine/ConsoleHelpers.cs b/src/System.CommandLine/ConsoleHelpers.cs index 2684413c89..36a72073a6 100644 --- a/src/System.CommandLine/ConsoleHelpers.cs +++ b/src/System.CommandLine/ConsoleHelpers.cs @@ -5,7 +5,8 @@ namespace System.CommandLine { - internal static class ConsoleHelpers + // TODO: Added to project and made public for use in ErrorReportingSubsystem + public static class ConsoleHelpers { private static readonly bool ColorsAreSupported = GetColorsAreSupported(); @@ -20,7 +21,7 @@ private static bool GetColorsAreSupported() #endif && !Console.IsOutputRedirected; - internal static void SetTerminalForegroundRed() + public static void SetTerminalForegroundRed() { if (ColorsAreSupported) { @@ -28,7 +29,7 @@ internal static void SetTerminalForegroundRed() } } - internal static void ResetTerminalForegroundColor() + public static void ResetTerminalForegroundColor() { if (ColorsAreSupported) { diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 6cd32ef4d8..159e6d1353 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -46,6 +46,7 @@ + From 12578721aae6858638d0de49192b576e83eb7026 Mon Sep 17 00:00:00 2001 From: Jean Joeris Date: Sat, 23 Mar 2024 14:22:35 -0400 Subject: [PATCH 055/150] Add unit tests for ErrorReportingSubsystem, with scope changes --- .../ErrorReportingSubsystemTests.cs | 70 +++++++++++++++++++ ...System.CommandLine.Subsystems.Tests.csproj | 1 + src/System.CommandLine/Parsing/ParseError.cs | 2 +- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs diff --git a/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs new file mode 100644 index 0000000000..b3a587fa69 --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs @@ -0,0 +1,70 @@ +// 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.Reflection; +using FluentAssertions; +using Xunit; +using System.CommandLine.Parsing; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace System.CommandLine.Subsystems.Tests +{ + public class ErrorReportingSubsystemTests + { + [Fact] + public void Report_when_single_error_writes_to_console_hack() + { + var error = new ParseError("a sweet error message"); + var errors = new List { error }; + var errorSubsystem = new ErrorReportingSubsystem(); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + + errorSubsystem.Report(consoleHack, errors); + + consoleHack.GetBuffer().Trim().Should().Be(error.Message); + } + + [Fact] + public void Report_when_multiple_error_writes_to_console_hack() + { + var error = new ParseError("a sweet error message"); + var anotherError = new ParseError("another sweet error message"); + var errors = new List { error, anotherError }; + var errorSubsystem = new ErrorReportingSubsystem(); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + + errorSubsystem.Report(consoleHack, errors); + + consoleHack.GetBuffer().Trim().Should().Be($"{error.Message}\r\n{anotherError.Message}"); + } + + [Fact] + public void Report_when_no_errors_writes_nothing_to_console_hack() + { + var errors = new List { }; + var errorSubsystem = new ErrorReportingSubsystem(); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + + errorSubsystem.Report(consoleHack, errors); + + consoleHack.GetBuffer().Trim().Should().Be(""); + } + + [Theory] + [InlineData("-v", false)] + [InlineData("-x", true)] + [InlineData("", false)] + public void GetIsActivated_tests(string input, bool result) + { + var rootCommand = new CliRootCommand {new CliOption("-v")}; + var configuration = new CliConfiguration(rootCommand); + var errorSubsystem = new ErrorReportingSubsystem(); + Subsystem.Initialize(errorSubsystem, configuration); + + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var isActive = Subsystem.GetIsActivated(errorSubsystem, parseResult); + + isActive.Should().Be(result); + } + } +} diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index 7af1fa8222..a57056262c 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -39,6 +39,7 @@ + diff --git a/src/System.CommandLine/Parsing/ParseError.cs b/src/System.CommandLine/Parsing/ParseError.cs index 5c5453a48e..db3d7e73ab 100644 --- a/src/System.CommandLine/Parsing/ParseError.cs +++ b/src/System.CommandLine/Parsing/ParseError.cs @@ -10,7 +10,7 @@ public sealed class ParseError { // TODO: add position // TODO: reevaluate whether we should be exposing a SymbolResult here - internal ParseError( + public ParseError( string message, SymbolResult? symbolResult = null) { From ab679bc57fde91bf00650498bb0d19cb56f42399 Mon Sep 17 00:00:00 2001 From: Jean Joeris Date: Sat, 23 Mar 2024 14:28:31 -0400 Subject: [PATCH 056/150] Move old ParseErrorReportingTests to ErrorReportingFunctionalTests --- .../ErrorReportingFunctionalTests.cs} | 7 ++++++- .../System.CommandLine.Subsystems.Tests.csproj | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) rename src/{System.CommandLine.Tests/ParseErrorReportingTests.cs => System.CommandLine.Subsystems.Tests/ErrorReportingFunctionalTests.cs} (96%) diff --git a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs b/src/System.CommandLine.Subsystems.Tests/ErrorReportingFunctionalTests.cs similarity index 96% rename from src/System.CommandLine.Tests/ParseErrorReportingTests.cs rename to src/System.CommandLine.Subsystems.Tests/ErrorReportingFunctionalTests.cs index e6e4b21137..3ca0724a66 100644 --- a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ErrorReportingFunctionalTests.cs @@ -1,8 +1,10 @@ // 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.Help; using System.CommandLine.Invocation; +*/ using System.IO; using FluentAssertions; using Xunit; @@ -12,8 +14,10 @@ namespace System.CommandLine.Tests; -public class ParseErrorReportingTests +public class ErrorReportingFunctionalTests { + // TODO: these tests depend on help output + /* [Fact] // https://github.com/dotnet/command-line-api/issues/817 public void Parse_error_reporting_reports_error_when_help_is_used_and_required_subcommand_is_missing() { @@ -126,4 +130,5 @@ public void When_no_help_option_is_present_then_help_is_not_shown_for_parse_erro output.ToString().Should().NotShowHelp(); } + */ } \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index a57056262c..2366d013b0 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -39,6 +39,7 @@ + From a6dc1967b47931469af296463566d2dff3695868 Mon Sep 17 00:00:00 2001 From: Jean Joeris Date: Mon, 1 Apr 2024 11:42:48 -0400 Subject: [PATCH 057/150] Cleanup formatting and make CI pass Fix CI Use environment newline --- .../ErrorReportingFunctionalTests.cs | 2 +- .../ErrorReportingSubsystemTests.cs | 94 +++++++++---------- .../ErrorReportingSubsystem.cs | 5 +- 3 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ErrorReportingFunctionalTests.cs b/src/System.CommandLine.Subsystems.Tests/ErrorReportingFunctionalTests.cs index 3ca0724a66..47e4808c18 100644 --- a/src/System.CommandLine.Subsystems.Tests/ErrorReportingFunctionalTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ErrorReportingFunctionalTests.cs @@ -131,4 +131,4 @@ public void When_no_help_option_is_present_then_help_is_not_shown_for_parse_erro output.ToString().Should().NotShowHelp(); } */ -} \ No newline at end of file +} diff --git a/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs index b3a587fa69..8e1f5c49e6 100644 --- a/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs @@ -1,70 +1,68 @@ // 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.Reflection; using FluentAssertions; using Xunit; using System.CommandLine.Parsing; -using static System.Runtime.InteropServices.JavaScript.JSType; -namespace System.CommandLine.Subsystems.Tests +namespace System.CommandLine.Subsystems.Tests; + +public class ErrorReportingSubsystemTests { - public class ErrorReportingSubsystemTests + [Fact] + public void Report_when_single_error_writes_to_console_hack() { - [Fact] - public void Report_when_single_error_writes_to_console_hack() - { - var error = new ParseError("a sweet error message"); - var errors = new List { error }; - var errorSubsystem = new ErrorReportingSubsystem(); - var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var error = new ParseError("a sweet error message"); + var errors = new List { error }; + var errorSubsystem = new ErrorReportingSubsystem(); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); - errorSubsystem.Report(consoleHack, errors); + errorSubsystem.Report(consoleHack, errors); - consoleHack.GetBuffer().Trim().Should().Be(error.Message); - } + consoleHack.GetBuffer().Trim().Should().Be(error.Message); + } - [Fact] - public void Report_when_multiple_error_writes_to_console_hack() - { - var error = new ParseError("a sweet error message"); - var anotherError = new ParseError("another sweet error message"); - var errors = new List { error, anotherError }; - var errorSubsystem = new ErrorReportingSubsystem(); - var consoleHack = new ConsoleHack().RedirectToBuffer(true); + [Fact] + public void Report_when_multiple_error_writes_to_console_hack() + { + var error = new ParseError("a sweet error message"); + var anotherError = new ParseError("another sweet error message"); + var errors = new List { error, anotherError }; + var errorSubsystem = new ErrorReportingSubsystem(); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); - errorSubsystem.Report(consoleHack, errors); + errorSubsystem.Report(consoleHack, errors); - consoleHack.GetBuffer().Trim().Should().Be($"{error.Message}\r\n{anotherError.Message}"); - } + consoleHack.GetBuffer().Trim().Should().Be($"{error.Message}{Environment.NewLine}{anotherError.Message}"); + } - [Fact] - public void Report_when_no_errors_writes_nothing_to_console_hack() - { - var errors = new List { }; - var errorSubsystem = new ErrorReportingSubsystem(); - var consoleHack = new ConsoleHack().RedirectToBuffer(true); + [Fact] + public void Report_when_no_errors_writes_nothing_to_console_hack() + { + var errors = new List { }; + var errorSubsystem = new ErrorReportingSubsystem(); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); - errorSubsystem.Report(consoleHack, errors); + errorSubsystem.Report(consoleHack, errors); - consoleHack.GetBuffer().Trim().Should().Be(""); - } + consoleHack.GetBuffer().Trim().Should().Be(""); + } - [Theory] - [InlineData("-v", false)] - [InlineData("-x", true)] - [InlineData("", false)] - public void GetIsActivated_tests(string input, bool result) - { - var rootCommand = new CliRootCommand {new CliOption("-v")}; - var configuration = new CliConfiguration(rootCommand); - var errorSubsystem = new ErrorReportingSubsystem(); - Subsystem.Initialize(errorSubsystem, configuration); + [Theory] + [InlineData("-v", false)] + [InlineData("-x", true)] + [InlineData("", false)] + public void GetIsActivated_tests(string input, bool result) + { + var rootCommand = new CliRootCommand {new CliOption("-v")}; + var configuration = new CliConfiguration(rootCommand); + var errorSubsystem = new ErrorReportingSubsystem(); + IReadOnlyList args = [""]; + Subsystem.Initialize(errorSubsystem, configuration, args); - var parseResult = CliParser.Parse(rootCommand, input, configuration); - var isActive = Subsystem.GetIsActivated(errorSubsystem, parseResult); + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var isActive = Subsystem.GetIsActivated(errorSubsystem, parseResult); - isActive.Should().Be(result); - } + isActive.Should().Be(result); } } diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs index 7104f2781b..cfe1956a70 100644 --- a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -8,9 +8,10 @@ namespace System.CommandLine; /// -/// +/// Subsystem for reporting errors /// /// +/// This class, including interface, is likey to change as powderhouse continues /// public class ErrorReportingSubsystem : CliSubsystem { @@ -21,6 +22,7 @@ public ErrorReportingSubsystem(IAnnotationProvider? annotationProvider = null) protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.Errors.Any(); + // TODO: properly test execute directly when parse result is usable in tests protected internal override CliExit Execute(PipelineContext pipelineContext) { var _ = pipelineContext.ParseResult @@ -31,7 +33,6 @@ protected internal override CliExit Execute(PipelineContext pipelineContext) return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); } - // TODO: internal, protected virtual? public void Report(ConsoleHack consoleHack, IReadOnlyList errors) { ConsoleHelpers.ResetTerminalForegroundColor(); From f1f6f24bb5a99013364906a21f830b65d3f423b0 Mon Sep 17 00:00:00 2001 From: Jean Joeris Date: Thu, 25 Apr 2024 14:44:09 -0400 Subject: [PATCH 058/150] Break up GetIsActivated test cases for clarity --- .../ErrorReportingSubsystemTests.cs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs index 8e1f5c49e6..1ce30a9422 100644 --- a/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ErrorReportingSubsystemTests.cs @@ -49,10 +49,9 @@ public void Report_when_no_errors_writes_nothing_to_console_hack() } [Theory] - [InlineData("-v", false)] - [InlineData("-x", true)] - [InlineData("", false)] - public void GetIsActivated_tests(string input, bool result) + [InlineData("-x")] + [InlineData("-non_existant_option")] + public void GetIsActivated_GivenInvalidInput_SubsystemIsActive(string input) { var rootCommand = new CliRootCommand {new CliOption("-v")}; var configuration = new CliConfiguration(rootCommand); @@ -63,6 +62,23 @@ public void GetIsActivated_tests(string input, bool result) var parseResult = CliParser.Parse(rootCommand, input, configuration); var isActive = Subsystem.GetIsActivated(errorSubsystem, parseResult); - isActive.Should().Be(result); + isActive.Should().BeTrue(); + } + + [Theory] + [InlineData("-v")] + [InlineData("")] + public void GetIsActivated_GivenValidInput_SubsystemShouldNotBeActive(string input) + { + var rootCommand = new CliRootCommand { new CliOption("-v") }; + var configuration = new CliConfiguration(rootCommand); + var errorSubsystem = new ErrorReportingSubsystem(); + IReadOnlyList args = [""]; + Subsystem.Initialize(errorSubsystem, configuration, args); + + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var isActive = Subsystem.GetIsActivated(errorSubsystem, parseResult); + + isActive.Should().BeFalse(); } } From d0e945f8d48d9a0d30c3ac5ebf1fda45a2d715d5 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 13 Apr 2024 07:57:48 -0400 Subject: [PATCH 059/150] Made ArgumentResult, OptionResult and CommandResult internal and fixed using code. --- .../Directives/DiagramSubsystem.cs | 2 +- src/System.CommandLine/CliArgument{T}.cs | 2 +- src/System.CommandLine/CliOption{T}.cs | 2 +- src/System.CommandLine/ParseResult.cs | 10 +++++----- src/System.CommandLine/Parsing/ArgumentResult.cs | 2 +- src/System.CommandLine/Parsing/CommandResult.cs | 2 +- src/System.CommandLine/Parsing/OptionResult.cs | 2 +- src/System.CommandLine/Parsing/SymbolResult.cs | 6 +++--- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index a00e8c4734..41bcd96362 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -35,7 +35,7 @@ internal static StringBuilder Diagram(ParseResult parseResult) var builder = new StringBuilder(100); - Diagram(builder, parseResult.RootCommandResult, parseResult); + //Diagram(builder, parseResult.RootCommandResult, parseResult); // TODO: Unmatched tokens /* diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index 7a83a03dc1..77c31edb7b 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -34,7 +34,7 @@ public CliArgument(string name) : base(name) /// the delegate is also invoked when an input was provided. /// */ - public Func? DefaultValueFactory { get; set; } + internal Func? DefaultValueFactory { get; set; } // TODO: custom parsers /* diff --git a/src/System.CommandLine/CliOption{T}.cs b/src/System.CommandLine/CliOption{T}.cs index fff51402d1..1a233183ad 100644 --- a/src/System.CommandLine/CliOption{T}.cs +++ b/src/System.CommandLine/CliOption{T}.cs @@ -30,7 +30,7 @@ private protected CliOption(string name, string[] aliases, CliArgument argume } /// - public Func? DefaultValueFactory + internal Func? DefaultValueFactory { get => _argument.DefaultValueFactory; set => _argument.DefaultValueFactory = value; diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index c0001edb14..7c4e1a437f 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -87,7 +87,7 @@ internal ParseResult( /// /// A result indicating the command specified in the command line input. /// - public CommandResult CommandResult { get; } + internal CommandResult CommandResult { get; } /// /// The configuration used to produce the parse result. @@ -97,7 +97,7 @@ internal ParseResult( /// /// Gets the root command result. /// - public CommandResult RootCommandResult => _rootCommandResult; + internal CommandResult RootCommandResult => _rootCommandResult; /// /// Gets the parse errors found while parsing command line input. @@ -180,7 +180,7 @@ CommandLineText is null /// /// The argument for which to find a result. /// A result for the specified argument, or if it was not provided and no default was configured. - public ArgumentResult? GetResult(CliArgument argument) => + internal ArgumentResult? GetResult(CliArgument argument) => _rootCommandResult.GetResult(argument); /// @@ -188,7 +188,7 @@ CommandLineText is null /// /// The command for which to find a result. /// A result for the specified command, or if it was not provided. - public CommandResult? GetResult(CliCommand command) => + internal CommandResult? GetResult(CliCommand command) => _rootCommandResult.GetResult(command); /// @@ -196,7 +196,7 @@ CommandLineText is null /// /// The option for which to find a result. /// A result for the specified option, or if it was not provided and no default was configured. - public OptionResult? GetResult(CliOption option) => + internal OptionResult? GetResult(CliOption option) => _rootCommandResult.GetResult(option); // TODO: Directives diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/ArgumentResult.cs index 1a7c6e4cd7..9e2248b870 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/ArgumentResult.cs @@ -9,7 +9,7 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing an . /// - public sealed class ArgumentResult : SymbolResult + internal sealed class ArgumentResult : SymbolResult { private ArgumentConversionResult? _conversionResult; private bool _onlyTakeHasBeenCalled; diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CommandResult.cs index 7adf32d08f..f486794eba 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CommandResult.cs @@ -9,7 +9,7 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing a . /// - public sealed class CommandResult : SymbolResult + internal sealed class CommandResult : SymbolResult { internal CommandResult( CliCommand command, diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/OptionResult.cs index 9892067216..ccba50a386 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/OptionResult.cs @@ -10,7 +10,7 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing an . /// - public sealed class OptionResult : SymbolResult + internal sealed class OptionResult : SymbolResult { private ArgumentConversionResult? _argumentConversionResult; diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/SymbolResult.cs index 25b1c17b08..38da2e21c1 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/SymbolResult.cs @@ -70,21 +70,21 @@ public IEnumerable Errors /// /// The argument for which to find a result. /// An argument result if the argument was matched by the parser or has a default value; otherwise, null. - public ArgumentResult? GetResult(CliArgument argument) => SymbolResultTree.GetResult(argument); + internal ArgumentResult? GetResult(CliArgument argument) => SymbolResultTree.GetResult(argument); /// /// Finds a result for the specific command anywhere in the parse tree, including parent and child symbol results. /// /// The command for which to find a result. /// An command result if the command was matched by the parser; otherwise, null. - public CommandResult? GetResult(CliCommand command) => SymbolResultTree.GetResult(command); + internal CommandResult? GetResult(CliCommand command) => SymbolResultTree.GetResult(command); /// /// Finds a result for the specific option anywhere in the parse tree, including parent and child symbol results. /// /// The option for which to find a result. /// An option result if the option was matched by the parser or has a default value; otherwise, null. - public OptionResult? GetResult(CliOption option) => SymbolResultTree.GetResult(option); + internal OptionResult? GetResult(CliOption option) => SymbolResultTree.GetResult(option); // TODO: directives /* From a40b5b6f02beee3b4732ace8287de66f7f8a1b54 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 20 Apr 2024 11:41:45 -0400 Subject: [PATCH 060/150] Renamed Start to Index Start may imply position in string, and this is position in the args array or response file. --- src/System.CommandLine/Parsing/Location.cs | 8 ++++---- src/System.CommandLine/Parsing/StringExtensions.cs | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/System.CommandLine/Parsing/Location.cs b/src/System.CommandLine/Parsing/Location.cs index 18d97ce681..30d3a5285e 100644 --- a/src/System.CommandLine/Parsing/Location.cs +++ b/src/System.CommandLine/Parsing/Location.cs @@ -29,11 +29,11 @@ internal static Location CreateResponse(string responseSourceName, int start, Lo internal static Location FromOuterLocation(string text, int start, Location outerLocation, int offset = 0) => new(text, outerLocation.Source, start, outerLocation, offset); - public Location(string text, string source, int start, Location? outerLocation, int offset = 0) + public Location(string text, string source, int index, Location? outerLocation, int offset = 0) { Text = text; Source = source; - Start = start; + Index = index; Length = text.Length; Offset = offset; OuterLocation = outerLocation; @@ -41,7 +41,7 @@ public Location(string text, string source, int start, Location? outerLocation, public string Text { get; } public string Source { get; } - public int Start { get; } + public int Index { get; } public int Offset { get; } public int Length { get; } public Location? OuterLocation { get; } @@ -50,7 +50,7 @@ public bool IsImplicit => Source == Implicit; public override string ToString() - => $"{(OuterLocation is null ? "" : OuterLocation.ToString() + "; ")}{Text} from {Source}[{Start}, {Length}, {Offset}]"; + => $"{(OuterLocation is null ? "" : OuterLocation.ToString() + "; ")}{Text} from {Source}[{Index}, {Length}, {Offset}]"; } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 9927aa4dd9..93821aa57d 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -78,7 +78,7 @@ internal static void Tokenize( var maxSkippedPositions = configuration.PreProcessedLocations is null || !configuration.PreProcessedLocations.Any() ? 0 - : configuration.PreProcessedLocations.Max(x => x.Start); + : configuration.PreProcessedLocations.Max(x => x.Index); var validTokens = GetValidTokens(rootCommand); @@ -111,7 +111,7 @@ internal static void Tokenize( if (i <= maxSkippedPositions && configuration.PreProcessedLocations is not null - && configuration.PreProcessedLocations.Any(x => x.Start == i)) + && configuration.PreProcessedLocations.Any(x => x.Index == i)) { continue; } @@ -229,7 +229,7 @@ static bool TryUnbundle(ReadOnlySpan alias, { string value = alias.Slice(i + 1).ToString(); tokenList.Add(Argument(value, - Location.FromOuterLocation(value, outerLocation.Start, outerLocation, i + 1))); + Location.FromOuterLocation(value, outerLocation.Index, outerLocation, i + 1))); return true; } @@ -241,7 +241,7 @@ static bool TryUnbundle(ReadOnlySpan alias, // Invalid_char_in_bundle_causes_rest_to_be_interpreted_as_value string value = alias.Slice(i).ToString(); tokenList.Add(Argument(value, - Location.FromOuterLocation(value, outerLocation.Start, outerLocation, i))); + Location.FromOuterLocation(value, outerLocation.Index, outerLocation, i))); return true; } @@ -249,7 +249,7 @@ static bool TryUnbundle(ReadOnlySpan alias, } tokenList.Add(new CliToken(candidate, found.TokenType, found.Symbol, - Location.FromOuterLocation(candidate, outerLocation.Start, outerLocation, i + 1))); + Location.FromOuterLocation(candidate, outerLocation.Index, outerLocation, i + 1))); if (i != alias.Length - 1 && ((CliOption)found.Symbol).Greedy) { @@ -260,7 +260,7 @@ static bool TryUnbundle(ReadOnlySpan alias, } string value = alias.Slice(index).ToString(); - tokenList.Add(Argument(value, Location.FromOuterLocation(value, outerLocation.Start, outerLocation, index))); + tokenList.Add(Argument(value, Location.FromOuterLocation(value, outerLocation.Index, outerLocation, index))); return true; } } From fd917c87834e29a476b4c7108f424c54bd817c7e Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Fri, 26 Apr 2024 10:23:34 -0400 Subject: [PATCH 061/150] Made pipeline creation static for better Empty/standard ergonomics --- .../PipelineTests.cs | 15 +++++-------- .../VersionFunctionalTests.cs | 2 +- .../VersionSubsystemTests.cs | 17 +++++--------- src/System.CommandLine.Subsystems/Pipeline.cs | 22 +++++++++++++++++++ .../StandardPipeline.cs | 17 -------------- .../Subsystems/PipelineContext.cs | 2 +- 6 files changed, 34 insertions(+), 41 deletions(-) delete mode 100644 src/System.CommandLine.Subsystems/StandardPipeline.cs diff --git a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs index b99d9f1b72..b3124e416c 100644 --- a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs @@ -10,10 +10,7 @@ namespace System.CommandLine.Subsystems.Tests public class PipelineTests { private static Pipeline GetTestPipeline(VersionSubsystem versionSubsystem) - => new() - { - Version = versionSubsystem - }; + => Pipeline.Create(version: versionSubsystem); private static CliConfiguration GetNewTestConfiguration() => new(new CliRootCommand { new CliOption("-x") }); // Add option expected by test data @@ -156,7 +153,7 @@ public void Subsystem_runs_when_requested_even_when_there_are_errors(string inpu [Fact] public void Standard_pipeline_contains_expected_subsystems() { - var pipeline = new StandardPipeline(); + var pipeline = Pipeline.Create(); pipeline.Version.Should().BeOfType(); pipeline.Help.Should().BeOfType(); pipeline.ErrorReporting.Should().BeOfType(); @@ -166,7 +163,7 @@ public void Standard_pipeline_contains_expected_subsystems() [Fact] public void Normal_pipeline_contains_no_subsystems() { - var pipeline = new Pipeline(); + var pipeline = Pipeline.CreateEmpty();; pipeline.Version.Should().BeNull(); pipeline.Help.Should().BeNull(); pipeline.ErrorReporting.Should().BeNull(); @@ -179,10 +176,8 @@ public void Subsystems_can_access_each_others_data() // TODO: Explore a mechanism that doesn't require the reference to retrieve data, this shows that it is awkward var symbol = new CliOption("-x"); var console = GetNewTestConsole(); - var pipeline = new StandardPipeline - { - Version = new AlternateSubsystems.VersionThatUsesHelpData(symbol) - }; + var pipeline = Pipeline.Create(version : new AlternateSubsystems.VersionThatUsesHelpData(symbol)); + if (pipeline.Help is null) throw new InvalidOperationException(); var rootCommand = new CliRootCommand { diff --git a/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs index 48784745ab..abd979fd96 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs @@ -19,7 +19,7 @@ public class VersionFunctionalTests public void When_the_version_option_is_specified_then_the_version_is_written_to_standard_out() { var configuration = new CliConfiguration(new CliRootCommand()); - var pipeline = new Pipeline(); + var pipeline = Pipeline.Create(); var consoleHack = new ConsoleHack().RedirectToBuffer(true); pipeline.Version = new VersionSubsystem(); diff --git a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs index 304c12b6da..b977c4e5dd 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs @@ -17,10 +17,7 @@ public void When_version_subsystem_is_used_the_version_option_is_added_to_the_ro new CliOption("-x") // add option that is expected for the test data used here }; var configuration = new CliConfiguration(rootCommand); - var pipeline = new Pipeline - { - Version = new VersionSubsystem() - }; + var pipeline = Pipeline.Create(version: new VersionSubsystem()); // Parse is used because directly calling Initialize would be unusual var result = pipeline.Parse(configuration, ""); @@ -98,10 +95,8 @@ public void Console_output_can_be_tested() public void Custom_version_subsystem_can_be_used() { var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var pipeline = new Pipeline - { - Version = new AlternateSubsystems.AlternateVersion() - }; + var pipeline = Pipeline.Create(version: new AlternateSubsystems.AlternateVersion()); + pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); } @@ -110,10 +105,8 @@ public void Custom_version_subsystem_can_be_used() public void Custom_version_subsystem_can_replace_standard() { var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var pipeline = new StandardPipeline - { - Version = new AlternateSubsystems.AlternateVersion() - }; + var pipeline = Pipeline.Create(version: new AlternateSubsystems.AlternateVersion()); + pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); } diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index ec7aea776a..e3ca8e9bf3 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -9,11 +9,33 @@ namespace System.CommandLine; public class Pipeline { + public static Pipeline Create(HelpSubsystem? help = null, + VersionSubsystem? version = null, + CompletionSubsystem? completion = null, + DiagramSubsystem? diagram = null, + ErrorReportingSubsystem? errorReporting = null, + ValueSubsystem? value = null) + => new() + { + Help = help ?? new HelpSubsystem(), + Version = version ?? new VersionSubsystem(), + Completion = completion ?? new CompletionSubsystem(), + Diagram = diagram ?? new DiagramSubsystem(), + ErrorReporting = errorReporting ?? new ErrorReportingSubsystem(), + Value = value ?? new ValueSubsystem() + }; + + public static Pipeline CreateEmpty() + => new(); + + private Pipeline() { } + public HelpSubsystem? Help { get; set; } public VersionSubsystem? Version { get; set; } public CompletionSubsystem? Completion { get; set; } public DiagramSubsystem? Diagram { get; set; } public ErrorReportingSubsystem? ErrorReporting { get; set; } + public ValueSubsystem? Value { get; set; } public ParseResult Parse(CliConfiguration configuration, string rawInput) => Parse(configuration, CliParser.SplitCommandLine(rawInput).ToArray()); diff --git a/src/System.CommandLine.Subsystems/StandardPipeline.cs b/src/System.CommandLine.Subsystems/StandardPipeline.cs deleted file mode 100644 index 6e5792ebfe..0000000000 --- a/src/System.CommandLine.Subsystems/StandardPipeline.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. - -using System.CommandLine.Directives; - -namespace System.CommandLine; - -public class StandardPipeline : Pipeline -{ - public StandardPipeline() { - Help = new HelpSubsystem(); - Version = new VersionSubsystem(); - Completion = new CompletionSubsystem(); - Diagram = new DiagramSubsystem(); - ErrorReporting = new ErrorReportingSubsystem(); - } -} diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs b/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs index 847c328678..e074645533 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs @@ -7,7 +7,7 @@ public class PipelineContext(ParseResult? parseResult, string rawInput, Pipeline { public ParseResult? ParseResult { get; } = parseResult; public string RawInput { get; } = rawInput; - public Pipeline Pipeline { get; } = pipeline ?? new Pipeline(); + public Pipeline Pipeline { get; } = pipeline ?? Pipeline.Create(); public ConsoleHack ConsoleHack { get; } = consoleHack ?? new ConsoleHack(); public bool AlreadyHandled { get; set; } From bdcab5e0bbd9bc23499570afa0b04d9cd18d194b Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 27 Apr 2024 21:03:31 -0400 Subject: [PATCH 062/150] Fixed some places Create should have been CreateEmpty --- src/System.CommandLine.Subsystems.Tests/PipelineTests.cs | 7 ++++++- .../VersionFunctionalTests.cs | 2 +- .../VersionSubsystemTests.cs | 9 ++++++--- .../Subsystems/PipelineContext.cs | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs index b3124e416c..396da0181f 100644 --- a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs @@ -10,7 +10,12 @@ namespace System.CommandLine.Subsystems.Tests public class PipelineTests { private static Pipeline GetTestPipeline(VersionSubsystem versionSubsystem) - => Pipeline.Create(version: versionSubsystem); + { + var pipeline = Pipeline.CreateEmpty(); + pipeline.Version = versionSubsystem; + return pipeline; + } + private static CliConfiguration GetNewTestConfiguration() => new(new CliRootCommand { new CliOption("-x") }); // Add option expected by test data diff --git a/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs index abd979fd96..dfb5681a53 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs @@ -19,7 +19,7 @@ public class VersionFunctionalTests public void When_the_version_option_is_specified_then_the_version_is_written_to_standard_out() { var configuration = new CliConfiguration(new CliRootCommand()); - var pipeline = Pipeline.Create(); + var pipeline = Pipeline.CreateEmpty(); var consoleHack = new ConsoleHack().RedirectToBuffer(true); pipeline.Version = new VersionSubsystem(); diff --git a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs index b977c4e5dd..1c7501aceb 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs @@ -17,7 +17,8 @@ public void When_version_subsystem_is_used_the_version_option_is_added_to_the_ro new CliOption("-x") // add option that is expected for the test data used here }; var configuration = new CliConfiguration(rootCommand); - var pipeline = Pipeline.Create(version: new VersionSubsystem()); + var pipeline = Pipeline.CreateEmpty(); + pipeline.Version = new VersionSubsystem(); // Parse is used because directly calling Initialize would be unusual var result = pipeline.Parse(configuration, ""); @@ -95,7 +96,8 @@ public void Console_output_can_be_tested() public void Custom_version_subsystem_can_be_used() { var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var pipeline = Pipeline.Create(version: new AlternateSubsystems.AlternateVersion()); + var pipeline = Pipeline.CreateEmpty(); + pipeline.Version = new AlternateSubsystems.AlternateVersion(); pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); @@ -105,7 +107,8 @@ public void Custom_version_subsystem_can_be_used() public void Custom_version_subsystem_can_replace_standard() { var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var pipeline = Pipeline.Create(version: new AlternateSubsystems.AlternateVersion()); + var pipeline = Pipeline.CreateEmpty(); + pipeline.Version = new AlternateSubsystems.AlternateVersion(); pipeline.Execute(new CliConfiguration(new CliRootCommand()), "-v", consoleHack); consoleHack.GetBuffer().Trim().Should().Be($"***{Constants.version}***"); diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs b/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs index e074645533..06f12dd84e 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs @@ -7,7 +7,7 @@ public class PipelineContext(ParseResult? parseResult, string rawInput, Pipeline { public ParseResult? ParseResult { get; } = parseResult; public string RawInput { get; } = rawInput; - public Pipeline Pipeline { get; } = pipeline ?? Pipeline.Create(); + public Pipeline Pipeline { get; } = pipeline ?? Pipeline.CreateEmpty(); public ConsoleHack ConsoleHack { get; } = consoleHack ?? new ConsoleHack(); public bool AlreadyHandled { get; set; } From 2acce54313d350bbfaad5772c64656d4c572d601 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 28 Apr 2024 08:35:43 -0400 Subject: [PATCH 063/150] Made help option lookup symbol, not string based --- .../HelpSubsystem.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index f332e9a0ae..2a9290401b 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -3,6 +3,7 @@ using System.CommandLine.Subsystems.Annotations; using System.CommandLine.Subsystems; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace System.CommandLine; @@ -18,31 +19,30 @@ namespace System.CommandLine; public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) : CliSubsystem(HelpAnnotations.Prefix, SubsystemKind.Help, annotationProvider) { - public void SetDescription(CliSymbol symbol, string description) - => SetAnnotation(symbol, HelpAnnotations.Description, description); + public CliOption HelpOption { get; } =new CliOption("--help", ["-h"]) + { + // TODO: Why don't we accept bool like any other bool option? + Arity = ArgumentArity.Zero + }; +public void SetDescription(CliSymbol symbol, string description) + => SetAnnotation(symbol, HelpAnnotations.Description, description); public string GetDescription(CliSymbol symbol) => TryGetAnnotation(symbol, HelpAnnotations.Description, out var value) ? value : ""; - public AnnotationAccessor Description => new(this, HelpAnnotations.Description); protected internal override CliConfiguration Initialize(InitializationContext context) { - var option = new CliOption("--help", ["-h"]) - { - // TODO: Why don't we accept bool like any other bool option? - Arity = ArgumentArity.Zero - }; - context.Configuration.RootCommand.Add(option); + context.Configuration.RootCommand.Add(HelpOption); return context.Configuration; } protected internal override bool GetIsActivated(ParseResult? parseResult) - => parseResult is not null && parseResult.GetValue("--help"); + => parseResult is not null && parseResult.GetValue(HelpOption); protected internal override CliExit Execute(PipelineContext pipelineContext) { From 6e2c40f35f8304617ca837f162a0bab8b4950d1c Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Tue, 30 Apr 2024 13:49:31 -0400 Subject: [PATCH 064/150] In response to review --- src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 41bcd96362..5168822b76 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -35,6 +35,7 @@ internal static StringBuilder Diagram(ParseResult parseResult) var builder = new StringBuilder(100); + // TODO: Reinstate this when ready to implement //Diagram(builder, parseResult.RootCommandResult, parseResult); // TODO: Unmatched tokens From 45383cec6bd6bc71ab5adb2aff8ca7d96a6be3e2 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Tue, 30 Apr 2024 13:58:20 -0400 Subject: [PATCH 065/150] In response to review --- .../HelpSubsystem.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index 2a9290401b..ca70785c93 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -3,7 +3,6 @@ using System.CommandLine.Subsystems.Annotations; using System.CommandLine.Subsystems; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace System.CommandLine; @@ -16,22 +15,24 @@ namespace System.CommandLine; // var command = new CliCommand("greet") // .With(help.Description, "Greet the user"); // -public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) +public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) : CliSubsystem(HelpAnnotations.Prefix, SubsystemKind.Help, annotationProvider) { - public CliOption HelpOption { get; } =new CliOption("--help", ["-h"]) - { - // TODO: Why don't we accept bool like any other bool option? - Arity = ArgumentArity.Zero - }; + public CliOption HelpOption { get; } = new CliOption("--help", ["-h"]) + { + // TODO: Why don't we accept bool like any other bool option? + Arity = ArgumentArity.Zero + }; -public void SetDescription(CliSymbol symbol, string description) + public void SetDescription(CliSymbol symbol, string description) => SetAnnotation(symbol, HelpAnnotations.Description, description); - public string GetDescription(CliSymbol symbol) + + public string GetDescription(CliSymbol symbol) => TryGetAnnotation(symbol, HelpAnnotations.Description, out var value) ? value : ""; - public AnnotationAccessor Description + + public AnnotationAccessor Description => new(this, HelpAnnotations.Description); protected internal override CliConfiguration Initialize(InitializationContext context) From 78aa0b43c64fd4c30dad340a19370f9bc9a932bc Mon Sep 17 00:00:00 2001 From: Jean Joeris Date: Wed, 1 May 2024 12:12:39 -0400 Subject: [PATCH 066/150] Move ConsoleHelpers to internal in subsystems --- .../ConsoleHelpers.cs | 7 +++---- .../ErrorReportingSubsystem.cs | 2 +- src/System.CommandLine/System.CommandLine.csproj | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) rename src/{System.CommandLine => System.CommandLine.Subsystems}/ConsoleHelpers.cs (83%) diff --git a/src/System.CommandLine/ConsoleHelpers.cs b/src/System.CommandLine.Subsystems/ConsoleHelpers.cs similarity index 83% rename from src/System.CommandLine/ConsoleHelpers.cs rename to src/System.CommandLine.Subsystems/ConsoleHelpers.cs index 36a72073a6..2684413c89 100644 --- a/src/System.CommandLine/ConsoleHelpers.cs +++ b/src/System.CommandLine.Subsystems/ConsoleHelpers.cs @@ -5,8 +5,7 @@ namespace System.CommandLine { - // TODO: Added to project and made public for use in ErrorReportingSubsystem - public static class ConsoleHelpers + internal static class ConsoleHelpers { private static readonly bool ColorsAreSupported = GetColorsAreSupported(); @@ -21,7 +20,7 @@ private static bool GetColorsAreSupported() #endif && !Console.IsOutputRedirected; - public static void SetTerminalForegroundRed() + internal static void SetTerminalForegroundRed() { if (ColorsAreSupported) { @@ -29,7 +28,7 @@ public static void SetTerminalForegroundRed() } } - public static void ResetTerminalForegroundColor() + internal static void ResetTerminalForegroundColor() { if (ColorsAreSupported) { diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs index cfe1956a70..ac5f1cb00b 100644 --- a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -26,7 +26,7 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) protected internal override CliExit Execute(PipelineContext pipelineContext) { var _ = pipelineContext.ParseResult - ?? throw new ArgumentNullException($"{nameof(pipelineContext)}.ParseResult"); + ?? throw new ArgumentException("The parse result has not been set", nameof(pipelineContext)); Report(pipelineContext.ConsoleHack, pipelineContext.ParseResult.Errors); diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 159e6d1353..6cd32ef4d8 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -46,7 +46,6 @@ - From bd6946901894e176c6aab0b8ba824cf508e32ce5 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Tue, 30 Apr 2024 11:42:38 -0700 Subject: [PATCH 067/150] refactor: Remove Get/Set Description on Helper subsystem Add Get to AnnotationAccessor --- .../AlternateSubsystems.cs | 8 +++----- src/System.CommandLine.Subsystems/HelpSubsystem.cs | 10 +--------- .../Subsystems/Annotations/AnnotationAccessor.cs | 10 +++++++++- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index b3ae527fe0..a419a9a6cc 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Directives; -using System.CommandLine.Subsystems; -using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine.Subsystems.Tests { @@ -33,7 +31,7 @@ public VersionThatUsesHelpData(CliSymbol symbol) protected override CliExit Execute(PipelineContext pipelineContext) { var help = pipelineContext.Pipeline.Help ?? throw new InvalidOperationException("Help cannot be null for this subsystem to work"); - var data = help.GetDescription(Symbol); + string data = help.Description.Get(Symbol); pipelineContext.ConsoleHack.WriteLine(data); pipelineContext.AlreadyHandled = true; @@ -68,11 +66,11 @@ protected override CliExit TearDown(CliExit cliExit) } internal class StringDirectiveSubsystem(IAnnotationProvider? annotationProvider = null) - : DirectiveSubsystem("other",SubsystemKind.Diagram, annotationProvider) + : DirectiveSubsystem("other", SubsystemKind.Diagram, annotationProvider) { } internal class BooleanDirectiveSubsystem(IAnnotationProvider? annotationProvider = null) - : DirectiveSubsystem("diagram", SubsystemKind.Diagram, annotationProvider) + : DirectiveSubsystem("diagram", SubsystemKind.Diagram, annotationProvider) { } } diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index ca70785c93..a8fe5ad655 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// 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.Subsystems.Annotations; @@ -24,14 +24,6 @@ public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) Arity = ArgumentArity.Zero }; - public void SetDescription(CliSymbol symbol, string description) - => SetAnnotation(symbol, HelpAnnotations.Description, description); - - public string GetDescription(CliSymbol symbol) - => TryGetAnnotation(symbol, HelpAnnotations.Description, out var value) - ? value - : ""; - public AnnotationAccessor Description => new(this, HelpAnnotations.Description); diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs index 622361f754..3bc91fafaa 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs @@ -8,7 +8,7 @@ namespace System.CommandLine.Subsystems.Annotations; /// /// Allows associating an annotation with a . The annotation will be stored by the accessor's owner . /// -public struct AnnotationAccessor(CliSubsystem owner, AnnotationId id) +public struct AnnotationAccessor(CliSubsystem owner, AnnotationId id, TValue? defaultValue = default) { /// /// The ID of the annotation @@ -16,4 +16,12 @@ public struct AnnotationAccessor(CliSubsystem owner, AnnotationId Id { get; } public readonly void Set(CliSymbol symbol, TValue value) => owner.SetAnnotation(symbol, id, value); public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, id, out value); + public readonly TValue? Get(CliSymbol symbol) + { + if (TryGet(symbol, out var value)) + { + return value ?? defaultValue; + } + return defaultValue; + } } From 494e6b2a7f7ea194a8c4afcdd59c3e080d9bda7c Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Wed, 1 May 2024 10:24:23 -0700 Subject: [PATCH 068/150] Use Property Id and use default value properly --- .../Subsystems/Annotations/AnnotationAccessor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs index 3bc91fafaa..9ea6aac8fe 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs @@ -13,14 +13,14 @@ public struct AnnotationAccessor(CliSubsystem owner, AnnotationId /// The ID of the annotation /// - public AnnotationId Id { get; } - public readonly void Set(CliSymbol symbol, TValue value) => owner.SetAnnotation(symbol, id, value); - public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, id, out value); + public AnnotationId Id { get; } = id; + public readonly void Set(CliSymbol symbol, TValue value) => owner.SetAnnotation(symbol, Id, value); + public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, Id, out value); public readonly TValue? Get(CliSymbol symbol) { if (TryGet(symbol, out var value)) { - return value ?? defaultValue; + return value; } return defaultValue; } From ff932a49e1b7d5fa03e40d645bc90da038a83bf0 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Fri, 3 May 2024 14:32:48 -0700 Subject: [PATCH 069/150] Update comments --- .../Annotations/AnnotationAccessor.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs index 9ea6aac8fe..66053d7859 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs @@ -8,14 +8,40 @@ namespace System.CommandLine.Subsystems.Annotations; /// /// Allows associating an annotation with a . The annotation will be stored by the accessor's owner . /// +/// +/// The annotation will be stored by the accessor's owner . +/// +/// The type of value to be stored +/// The subsystem that this annotation store data for. +/// The identifier for this annotation, since subsystems may have multiple annotations. +/// The default value to return if the annotation is not set. public struct AnnotationAccessor(CliSubsystem owner, AnnotationId id, TValue? defaultValue = default) { /// - /// The ID of the annotation + /// The identifier for this annotation, since subsystems may have multiple annotations. /// public AnnotationId Id { get; } = id; + + /// + /// Store a value for the annotation and symbol + /// + /// The CliSymbol the value is for. + /// The value to store. public readonly void Set(CliSymbol symbol, TValue value) => owner.SetAnnotation(symbol, Id, value); + + /// + /// Retrieve the value for the annotation and symbol + /// + /// The CliSymbol the value is for. + /// The value to retrieve/ + /// True if the value was found, false otherwise. public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, Id, out value); + + /// + /// Retrieve the value for the annotation and symbol + /// + /// The CliSymbol the value is for. + /// The retrieved value or if the value was not found. public readonly TValue? Get(CliSymbol symbol) { if (TryGet(symbol, out var value)) From 9e7bf7712a3fffb0402b521d27643e0b0a1862fc Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Fri, 3 May 2024 14:34:02 -0700 Subject: [PATCH 070/150] extra char in comment --- .../Subsystems/Annotations/AnnotationAccessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs index 66053d7859..b06cd9cb63 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs @@ -33,7 +33,7 @@ public struct AnnotationAccessor(CliSubsystem owner, AnnotationId /// The CliSymbol the value is for. - /// The value to retrieve/ + /// The value to retrieve. /// True if the value was found, false otherwise. public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, Id, out value); From fae9a17c47979192275f806fbbdcb20574fe82f5 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Tue, 7 May 2024 15:58:15 -0700 Subject: [PATCH 071/150] Remove get method --- .../AlternateSubsystems.cs | 4 ++-- .../Annotations/AnnotationAccessor.cs | 17 +---------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index a419a9a6cc..31679bbef5 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -31,9 +31,9 @@ public VersionThatUsesHelpData(CliSymbol symbol) protected override CliExit Execute(PipelineContext pipelineContext) { var help = pipelineContext.Pipeline.Help ?? throw new InvalidOperationException("Help cannot be null for this subsystem to work"); - string data = help.Description.Get(Symbol); + help.Description.TryGet(Symbol, out var description); - pipelineContext.ConsoleHack.WriteLine(data); + pipelineContext.ConsoleHack.WriteLine(description); pipelineContext.AlreadyHandled = true; return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs index b06cd9cb63..ec3b45e3c4 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs @@ -14,8 +14,7 @@ namespace System.CommandLine.Subsystems.Annotations; /// The type of value to be stored /// The subsystem that this annotation store data for. /// The identifier for this annotation, since subsystems may have multiple annotations. -/// The default value to return if the annotation is not set. -public struct AnnotationAccessor(CliSubsystem owner, AnnotationId id, TValue? defaultValue = default) +public struct AnnotationAccessor(CliSubsystem owner, AnnotationId id) { /// /// The identifier for this annotation, since subsystems may have multiple annotations. @@ -36,18 +35,4 @@ public struct AnnotationAccessor(CliSubsystem owner, AnnotationIdThe value to retrieve. /// True if the value was found, false otherwise. public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, Id, out value); - - /// - /// Retrieve the value for the annotation and symbol - /// - /// The CliSymbol the value is for. - /// The retrieved value or if the value was not found. - public readonly TValue? Get(CliSymbol symbol) - { - if (TryGet(symbol, out var value)) - { - return value; - } - return defaultValue; - } } From 917b29147fb75c9531646605b43270c8b6b67b25 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Tue, 16 Apr 2024 23:20:04 -0400 Subject: [PATCH 072/150] Added GetValue and other Get/TryGet methods Also did renames that were later changed again --- .../Annotations/ValueAnnotations.cs | 5 +- .../ValueSubsystem.cs | 142 +++++++++++++++--- 2 files changed, 127 insertions(+), 20 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs index b9a04ad7ad..87556f8d5e 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs @@ -12,6 +12,7 @@ public static class ValueAnnotations { public static string Prefix { get; } = nameof(SubsystemKind.Value); - public static AnnotationId Explicit { get; } = new(Prefix, nameof(Explicit)); - public static AnnotationId> Calculated { get; } = new(Prefix, nameof(Calculated)); + public static AnnotationId ExplicitDefault { get; } = new(Prefix, nameof(ExplicitDefault)); + public static AnnotationId?> DefaultCalculation { get; } = new(Prefix, nameof(DefaultCalculation)); + public static AnnotationId Value { get; } = new(Prefix, nameof(Value)); } diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index 1f2757bd51..e6e3808a19 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -9,35 +9,141 @@ namespace System.CommandLine; public class ValueSubsystem : CliSubsystem { - private ParseResult? parseResult = null; + // @mhutch: Is the TryGet on the sparse dictionaries how we should handle a case where the annotations will be sparse to support lazy? If so, should we have another method on + // the annotation wrapper, or an alternative struct when there a TryGet makes sense? This API needs review, maybe next Tuesday. + private PipelineContext? pipelineContext = null; public ValueSubsystem(IAnnotationProvider? annotationProvider = null) : base(ValueAnnotations.Prefix, SubsystemKind.Version, annotationProvider) - { } + { } - void SetExplicit(CliSymbol symbol, object value) - => SetAnnotation(symbol, ValueAnnotations.Explicit, value); - object GetExplicit(CliSymbol symbol) - => TryGetAnnotation(symbol, ValueAnnotations.Explicit, out var value) - ? value + internal void SetExplicitDefault(CliSymbol symbol, object? defaultValue) + => SetAnnotation(symbol, ValueAnnotations.ExplicitDefault, defaultValue); + internal object? GetExplicitDefault(CliSymbol symbol) + => TryGetAnnotation(symbol, ValueAnnotations.ExplicitDefault, out var defaultValue) + ? defaultValue : ""; - AnnotationAccessor Explicit - => new(this, ValueAnnotations.Explicit); + internal bool TryGetExplicitDefault(CliSymbol symbol, out T? defaultValue) + { + if (TryGetAnnotation(symbol, ValueAnnotations.Value, out var objectValue)) + { + defaultValue = (T)objectValue; + return true; + } + defaultValue = default; + return false; + } + public AnnotationAccessor ExplicitDefault + => new(this, ValueAnnotations.ExplicitDefault); - void SetCalculated(CliSymbol symbol, Func factory) - => SetAnnotation(symbol, ValueAnnotations.Calculated, factory); - Func? GetCalculatedValue(CliSymbol symbol) - => TryGetAnnotation>(symbol, ValueAnnotations.Calculated, out var value) + internal void SetDefaultCalculation(CliSymbol symbol, Func factory) + => SetAnnotation(symbol, ValueAnnotations.DefaultCalculation, factory); + internal Func? GetDefaultCalculation(CliSymbol symbol) + => TryGetAnnotation?>(symbol, ValueAnnotations.DefaultCalculation, out var value) ? value : null; + public AnnotationAccessor?> DefaultCalculation + => new(this, ValueAnnotations.DefaultCalculation); - AnnotationAccessor> Calculated - => new(this, ValueAnnotations.Calculated); + private void SetValue(CliSymbol symbol, object? value) + => SetAnnotation(symbol, ValueAnnotations.Value, value); + // TODO: Consider putting the logic in the generic version here + // TODO: Consider using a simple dictionary instead of the annotation (@mhutch) + // TODO: GetValue should call TryGetValue, not another call to TryGetAnnotation. + // TODO: Should we provide an untyped value? + private object? GetValue(CliSymbol symbol) + => TryGetAnnotation(symbol, ValueAnnotations.Value, out var value) + ? value + : null; + private bool TryGetValue(CliSymbol symbol, out T? value) + { + if (TryGetAnnotation(symbol, ValueAnnotations.Value, out var objectValue)) + { + value = (T)objectValue; + return true; + } + value = default; + return false; + } + // TODO: Is fluent style meaningful for Value? + //public AnnotationAccessor Value + // => new(this, ValueAnnotations.Value); protected internal override bool GetIsActivated(ParseResult? parseResult) + => true; + + protected internal override CliExit Execute(PipelineContext pipelineContext) { - this.parseResult = parseResult; - return true; + this.pipelineContext = pipelineContext; + return CliExit.NotRun(pipelineContext.ParseResult); } -} + // @mhutch: I find this more readable than the if conditional version below. There will be at least two more blocks. Look good? + public T? GetValue(CliSymbol symbol) + => symbol switch + { + { } when TryGetValue(symbol, out var value) + => value, // It has already been retrieved at least once + { } when pipelineContext?.ParseResult?.GetValueResult(symbol) is ValueResult valueResult + => UseValue(symbol, valueResult.GetValue()), // Value was supplied during parsing + // Value was not supplied during parsing, determine default now + { } when GetDefaultCalculation(symbol) is { } defaultValueCalculation + => UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)), + { } when TryGetExplicitDefault(symbol, out var explicitValue) => UseValue(symbol, explicitValue), + null => throw new ArgumentNullException(nameof(symbol)), + _ => UseValue(symbol, default(T)) + }; + + public T? GetValue2(CliSymbol symbol) + { + if (TryGetValue(symbol, out var value)) + { + // It has already been retrieved at least once + return value; + } + if (pipelineContext?.ParseResult?.GetValueResult(symbol) is ValueResult valueResult) + { + // Value was supplied during parsing + return UseValue(symbol, valueResult.GetValue()); + } + // Value was not supplied during parsing, determine default now + if (GetDefaultCalculation(symbol) is { } defaultValueCalculation) + { + return UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)); + } + if (TryGetExplicitDefault(symbol, out var explicitValue)) + { + return UseValue(symbol, value); + } + value = default; + SetValue(symbol, value); + return value; + + static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) + { + var objectValue = defaultValueCalculation(); + var value = objectValue is null + ? default + : (T)objectValue; + return value; + } + } + + + private static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) + { + var objectValue = defaultValueCalculation(); + var value = objectValue is null + ? default + : (T)objectValue; + return value; + } + + private T? UseValue(CliSymbol symbol, T? value) + { + SetValue(symbol, value); + return value; + } + + +} From dfa55d0a34e424a6c97fe102197577d7785bfb64 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Fri, 19 Apr 2024 11:31:12 -0400 Subject: [PATCH 073/150] Work on ValueSubsystem/added ValueSubsystemTests Worked on gathering ParseResult and improving Get and TryGet methods * Moved GetIsActivated and Execute to avoid splitting GetValue/TryGetValue * Updated the default value selection pipeline * Began writing tests --- ...System.CommandLine.Subsystems.Tests.csproj | 1 + .../TestData.cs | 14 +++ .../ValueSubsystemTests.cs | 43 +++++++ .../ValueSubsystem.cs | 115 +++++++++--------- 4 files changed, 115 insertions(+), 58 deletions(-) create mode 100644 src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index 2366d013b0..33a7efd6ef 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -32,6 +32,7 @@ --> + diff --git a/src/System.CommandLine.Subsystems.Tests/TestData.cs b/src/System.CommandLine.Subsystems.Tests/TestData.cs index 0460bb1d33..817d362d22 100644 --- a/src/System.CommandLine.Subsystems.Tests/TestData.cs +++ b/src/System.CommandLine.Subsystems.Tests/TestData.cs @@ -79,4 +79,18 @@ internal class Directive : IEnumerable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + internal class Value : IEnumerable + { + private readonly List _data = + [ + ["--intValue", 42], + ["--stringValue", "43"], + ["--boolValue", true] + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs new file mode 100644 index 0000000000..d829a7eebd --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -0,0 +1,43 @@ +// 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 FluentAssertions; +using System.CommandLine.Directives; +using System.CommandLine.Parsing; +using Xunit; + +namespace System.CommandLine.Subsystems.Tests; + +public class ValueSubsystemTests +{ + [Fact] + public void Value_is_always_activated() + { + CliRootCommand rootCommand = [new CliCommand("x")]; + var configuration = new CliConfiguration(rootCommand); + var subsystem = new ValueSubsystem(); + string[] args = ["x"]; + + Subsystem.Initialize(subsystem, configuration, args); + var parseResult = CliParser.Parse(rootCommand, args[0], configuration); + var isActive = Subsystem.GetIsActivated(subsystem, parseResult); + + isActive.Should().BeTrue(); + } + + [Theory] + [ClassData(typeof(TestData.Value))] + public void Diagram_is_activated_only_when_requested(string input, bool expectedIsActive) + { + CliRootCommand rootCommand = [new CliCommand("x")]; + var configuration = new CliConfiguration(rootCommand); + var subsystem = new DiagramSubsystem(); + var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + + Subsystem.Initialize(subsystem, configuration, args); + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var isActive = Subsystem.GetIsActivated(subsystem, parseResult); + + isActive.Should().Be(expectedIsActive); + } +} diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index e6e3808a19..60a29f2510 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -17,6 +17,7 @@ public ValueSubsystem(IAnnotationProvider? annotationProvider = null) : base(ValueAnnotations.Prefix, SubsystemKind.Version, annotationProvider) { } + //TODO: ExplicitDefault might have a valid null value, and thus try pattern. DefaultCalculation is only null when not present. Consider whether to use the same pattern (try on DefaultCalculation, even though it is not needed) internal void SetExplicitDefault(CliSymbol symbol, object? defaultValue) => SetAnnotation(symbol, ValueAnnotations.ExplicitDefault, defaultValue); internal object? GetExplicitDefault(CliSymbol symbol) @@ -45,16 +46,19 @@ internal void SetDefaultCalculation(CliSymbol symbol, Func factory) public AnnotationAccessor?> DefaultCalculation => new(this, ValueAnnotations.DefaultCalculation); - private void SetValue(CliSymbol symbol, object? value) + protected internal override bool GetIsActivated(ParseResult? parseResult) + => true; + + protected internal override CliExit Execute(PipelineContext pipelineContext) + { + this.pipelineContext = pipelineContext; + return CliExit.NotRun(pipelineContext.ParseResult); + } + + // TODO: Consider using a simple dictionary instead of the annotation (@mhutch) because with is not useful here + private void SetValue(CliSymbol symbol, object? value) => SetAnnotation(symbol, ValueAnnotations.Value, value); - // TODO: Consider putting the logic in the generic version here - // TODO: Consider using a simple dictionary instead of the annotation (@mhutch) - // TODO: GetValue should call TryGetValue, not another call to TryGetAnnotation. - // TODO: Should we provide an untyped value? - private object? GetValue(CliSymbol symbol) - => TryGetAnnotation(symbol, ValueAnnotations.Value, out var value) - ? value - : null; + // TODO: Consider a way to disallow CliCommand here, as it creates a pit of failure. private bool TryGetValue(CliSymbol symbol, out T? value) { if (TryGetAnnotation(symbol, ValueAnnotations.Value, out var objectValue)) @@ -69,17 +73,13 @@ private bool TryGetValue(CliSymbol symbol, out T? value) //public AnnotationAccessor Value // => new(this, ValueAnnotations.Value); - protected internal override bool GetIsActivated(ParseResult? parseResult) - => true; + public T? GetValue(CliOption option) + => GetValueInternal(option); + public T? GetValue(CliArgument argument) + => GetValueInternal(argument); - protected internal override CliExit Execute(PipelineContext pipelineContext) - { - this.pipelineContext = pipelineContext; - return CliExit.NotRun(pipelineContext.ParseResult); - } - - // @mhutch: I find this more readable than the if conditional version below. There will be at least two more blocks. Look good? - public T? GetValue(CliSymbol symbol) + // TODO: @mhutch: I find this more readable than the if conditional version below. There will be at least two more blocks. Look good? + private T? GetValueInternal(CliSymbol symbol) => symbol switch { { } when TryGetValue(symbol, out var value) @@ -89,46 +89,47 @@ protected internal override CliExit Execute(PipelineContext pipelineContext) // Value was not supplied during parsing, determine default now { } when GetDefaultCalculation(symbol) is { } defaultValueCalculation => UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)), - { } when TryGetExplicitDefault(symbol, out var explicitValue) => UseValue(symbol, explicitValue), + { } when TryGetExplicitDefault(symbol, out var explicitValue) + => UseValue(symbol, explicitValue), null => throw new ArgumentNullException(nameof(symbol)), _ => UseValue(symbol, default(T)) }; - public T? GetValue2(CliSymbol symbol) - { - if (TryGetValue(symbol, out var value)) - { - // It has already been retrieved at least once - return value; - } - if (pipelineContext?.ParseResult?.GetValueResult(symbol) is ValueResult valueResult) - { - // Value was supplied during parsing - return UseValue(symbol, valueResult.GetValue()); - } - // Value was not supplied during parsing, determine default now - if (GetDefaultCalculation(symbol) is { } defaultValueCalculation) - { - return UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)); - } - if (TryGetExplicitDefault(symbol, out var explicitValue)) - { - return UseValue(symbol, value); - } - value = default; - SetValue(symbol, value); - return value; - - static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) - { - var objectValue = defaultValueCalculation(); - var value = objectValue is null - ? default - : (T)objectValue; - return value; - } - } - + //// The following is temporarily included for showing why the above weird code is cleaner + //public T? GetValue2(CliSymbol symbol) + //{ + // if (TryGetValue(symbol, out var value)) + // { + // // It has already been retrieved at least once + // return value; + // } + // if (pipelineContext?.ParseResult?.GetValueResult(symbol) is ValueResult valueResult) + // { + // // Value was supplied during parsing + // return UseValue(symbol, valueResult.GetValue()); + // } + // // Value was not supplied during parsing, determine default now + // if (GetDefaultCalculation(symbol) is { } defaultValueCalculation) + // { + // return UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)); + // } + // if (TryGetExplicitDefault(symbol, out var explicitValue)) + // { + // return UseValue(symbol, value); + // } + // value = default; + // SetValue(symbol, value); + // return value; + + // static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) + // { + // var objectValue = defaultValueCalculation(); + // var value = objectValue is null + // ? default + // : (T)objectValue; + // return value; + // } + //} private static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) { @@ -141,9 +142,7 @@ protected internal override CliExit Execute(PipelineContext pipelineContext) private T? UseValue(CliSymbol symbol, T? value) { - SetValue(symbol, value); + SetValue(symbol, value); return value; } - - } From b2ac22584889c794c9e2a28f51ec3fe8335af14e Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 20 Apr 2024 10:48:31 -0400 Subject: [PATCH 074/150] Changes listed - updated Command/ValueResult, Value subsystem, and cleanup System.CommandLine: - CommandValueResult, made a tree - ValueResult - added text for later display, like `main`-ish - Created new `SymbolByName` - Cleanup ValueSubsystem - Renamed DefaultValue and CalculatedDefaultValue - Created cache for values - Limited GetValue/Try... to Arg and Option Test - Added ParseResultValueTest and tests in support of other work --- .../ValueSubsystemTests.cs | 34 +++++-- .../Annotations/ValueAnnotations.cs | 4 +- .../ValueSubsystem.cs | 94 ++++++------------- .../ParseResultValueTests.cs | 55 +++++++++++ src/System.CommandLine.Tests/ParserTests.cs | 32 ++++++- .../System.CommandLine.Tests.csproj | 3 +- src/System.CommandLine/ParseResult.cs | 66 ++++++++++++- .../Parsing/CommandValueResult.cs | 8 ++ .../Parsing/OptionResult.cs | 27 ++++-- src/System.CommandLine/Parsing/ValueResult.cs | 14 ++- 10 files changed, 239 insertions(+), 98 deletions(-) create mode 100644 src/System.CommandLine.Tests/ParseResultValueTests.cs diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index d829a7eebd..f5b57d4114 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -13,10 +13,15 @@ public class ValueSubsystemTests [Fact] public void Value_is_always_activated() { - CliRootCommand rootCommand = [new CliCommand("x")]; + CliRootCommand rootCommand = [ + new CliCommand("x") + { + new CliOption("--opt1") + }]; var configuration = new CliConfiguration(rootCommand); var subsystem = new ValueSubsystem(); - string[] args = ["x"]; + var input = "x --opt1 Kirk"; + var args = CliParser.SplitCommandLine(input).ToList(); Subsystem.Initialize(subsystem, configuration, args); var parseResult = CliParser.Parse(rootCommand, args[0], configuration); @@ -25,19 +30,28 @@ public void Value_is_always_activated() isActive.Should().BeTrue(); } - [Theory] - [ClassData(typeof(TestData.Value))] - public void Diagram_is_activated_only_when_requested(string input, bool expectedIsActive) + [Fact] + public void ValueSubsystem_returns_values_that_are_entered() { - CliRootCommand rootCommand = [new CliCommand("x")]; + CliRootCommand rootCommand = [ + new CliCommand("x") + { + new CliOption("--intValue"), + new CliOption("--stringValue"), + new CliOption("--boolValue") + }]; var configuration = new CliConfiguration(rootCommand); - var subsystem = new DiagramSubsystem(); - var args = CliParser.SplitCommandLine(input).ToList().AsReadOnly(); + var subsystem = new ValueSubsystem(); + const int expected1 = 42; + const string expected2 = "43"; + var input = $"x --intValue {expected1} --stringValue \"{expected2}\" --boolValue"; + var args = CliParser.SplitCommandLine(input).ToList(); Subsystem.Initialize(subsystem, configuration, args); var parseResult = CliParser.Parse(rootCommand, input, configuration); - var isActive = Subsystem.GetIsActivated(subsystem, parseResult); - isActive.Should().Be(expectedIsActive); + parseResult.GetValue("--intValue").Should().Be(expected1); + parseResult.GetValue("--stringValue").Should().Be(expected2); + parseResult.GetValue("--boolValue").Should().Be(true); } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs index 87556f8d5e..b867de30b2 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs @@ -12,7 +12,7 @@ public static class ValueAnnotations { public static string Prefix { get; } = nameof(SubsystemKind.Value); - public static AnnotationId ExplicitDefault { get; } = new(Prefix, nameof(ExplicitDefault)); - public static AnnotationId?> DefaultCalculation { get; } = new(Prefix, nameof(DefaultCalculation)); + public static AnnotationId DefaultValue { get; } = new(Prefix, nameof(DefaultValue)); + public static AnnotationId?> DefaultValueCalculation { get; } = new(Prefix, nameof(DefaultValueCalculation)); public static AnnotationId Value { get; } = new(Prefix, nameof(Value)); } diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index 60a29f2510..ff5020028b 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -12,19 +12,19 @@ public class ValueSubsystem : CliSubsystem // @mhutch: Is the TryGet on the sparse dictionaries how we should handle a case where the annotations will be sparse to support lazy? If so, should we have another method on // the annotation wrapper, or an alternative struct when there a TryGet makes sense? This API needs review, maybe next Tuesday. private PipelineContext? pipelineContext = null; + private Dictionary cachedValues = new(); public ValueSubsystem(IAnnotationProvider? annotationProvider = null) : base(ValueAnnotations.Prefix, SubsystemKind.Version, annotationProvider) { } - //TODO: ExplicitDefault might have a valid null value, and thus try pattern. DefaultCalculation is only null when not present. Consider whether to use the same pattern (try on DefaultCalculation, even though it is not needed) - internal void SetExplicitDefault(CliSymbol symbol, object? defaultValue) - => SetAnnotation(symbol, ValueAnnotations.ExplicitDefault, defaultValue); - internal object? GetExplicitDefault(CliSymbol symbol) - => TryGetAnnotation(symbol, ValueAnnotations.ExplicitDefault, out var defaultValue) + internal void SetDefaultValue(CliSymbol symbol, object? defaultValue) + => SetAnnotation(symbol, ValueAnnotations.DefaultValue, defaultValue); + internal object? GetDefaultValue(CliSymbol symbol) + => TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var defaultValue) ? defaultValue : ""; - internal bool TryGetExplicitDefault(CliSymbol symbol, out T? defaultValue) + internal bool TryGetDefaultValue(CliSymbol symbol, out T? defaultValue) { if (TryGetAnnotation(symbol, ValueAnnotations.Value, out var objectValue)) { @@ -34,17 +34,17 @@ internal bool TryGetExplicitDefault(CliSymbol symbol, out T? defaultValue) defaultValue = default; return false; } - public AnnotationAccessor ExplicitDefault - => new(this, ValueAnnotations.ExplicitDefault); + public AnnotationAccessor DefaultValue + => new(this, ValueAnnotations.DefaultValue); - internal void SetDefaultCalculation(CliSymbol symbol, Func factory) - => SetAnnotation(symbol, ValueAnnotations.DefaultCalculation, factory); - internal Func? GetDefaultCalculation(CliSymbol symbol) - => TryGetAnnotation?>(symbol, ValueAnnotations.DefaultCalculation, out var value) + internal void SetDefaultValueCalculation(CliSymbol symbol, Func factory) + => SetAnnotation(symbol, ValueAnnotations.DefaultValueCalculation, factory); + internal Func? GetDefaultValueCalculation(CliSymbol symbol) + => TryGetAnnotation?>(symbol, ValueAnnotations.DefaultValueCalculation, out var value) ? value : null; - public AnnotationAccessor?> DefaultCalculation - => new(this, ValueAnnotations.DefaultCalculation); + public AnnotationAccessor?> DefaultValueCalculation + => new(this, ValueAnnotations.DefaultValueCalculation); protected internal override bool GetIsActivated(ParseResult? parseResult) => true; @@ -55,82 +55,48 @@ protected internal override CliExit Execute(PipelineContext pipelineContext) return CliExit.NotRun(pipelineContext.ParseResult); } - // TODO: Consider using a simple dictionary instead of the annotation (@mhutch) because with is not useful here + + // TODO: Do it! Consider using a simple dictionary instead of the annotation (@mhutch) because with is not useful here private void SetValue(CliSymbol symbol, object? value) - => SetAnnotation(symbol, ValueAnnotations.Value, value); - // TODO: Consider a way to disallow CliCommand here, as it creates a pit of failure. + => cachedValues.Add(symbol, value); private bool TryGetValue(CliSymbol symbol, out T? value) { - if (TryGetAnnotation(symbol, ValueAnnotations.Value, out var objectValue)) + if (cachedValues.TryGetValue(symbol, out var objectValue)) { - value = (T)objectValue; + value = objectValue is null + ? default + :(T)objectValue; return true; } value = default; return false; } - // TODO: Is fluent style meaningful for Value? - //public AnnotationAccessor Value - // => new(this, ValueAnnotations.Value); public T? GetValue(CliOption option) => GetValueInternal(option); public T? GetValue(CliArgument argument) => GetValueInternal(argument); - // TODO: @mhutch: I find this more readable than the if conditional version below. There will be at least two more blocks. Look good? - private T? GetValueInternal(CliSymbol symbol) + private T? GetValueInternal(CliSymbol? symbol) => symbol switch { - { } when TryGetValue(symbol, out var value) + not null when TryGetValue(symbol, out var value) => value, // It has already been retrieved at least once - { } when pipelineContext?.ParseResult?.GetValueResult(symbol) is ValueResult valueResult + CliArgument argument when pipelineContext?.ParseResult?.GetValueResult(argument) is ValueResult valueResult // GetValue would always return a value + => UseValue(symbol, valueResult.GetValue()), // Value was supplied during parsing, + CliOption option when pipelineContext?.ParseResult?.GetValueResult(option) is ValueResult valueResult // GetValue would always return a value => UseValue(symbol, valueResult.GetValue()), // Value was supplied during parsing // Value was not supplied during parsing, determine default now - { } when GetDefaultCalculation(symbol) is { } defaultValueCalculation + not null when DefaultValueCalculation.TryGet(symbol, out var defaultValueCalculation) => UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)), - { } when TryGetExplicitDefault(symbol, out var explicitValue) + not null when TryGetDefaultValue(symbol, out var explicitValue) => UseValue(symbol, explicitValue), + //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) + // => UseValue(symbol, GetEnvByName(envName)), null => throw new ArgumentNullException(nameof(symbol)), _ => UseValue(symbol, default(T)) }; - //// The following is temporarily included for showing why the above weird code is cleaner - //public T? GetValue2(CliSymbol symbol) - //{ - // if (TryGetValue(symbol, out var value)) - // { - // // It has already been retrieved at least once - // return value; - // } - // if (pipelineContext?.ParseResult?.GetValueResult(symbol) is ValueResult valueResult) - // { - // // Value was supplied during parsing - // return UseValue(symbol, valueResult.GetValue()); - // } - // // Value was not supplied during parsing, determine default now - // if (GetDefaultCalculation(symbol) is { } defaultValueCalculation) - // { - // return UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)); - // } - // if (TryGetExplicitDefault(symbol, out var explicitValue)) - // { - // return UseValue(symbol, value); - // } - // value = default; - // SetValue(symbol, value); - // return value; - - // static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) - // { - // var objectValue = defaultValueCalculation(); - // var value = objectValue is null - // ? default - // : (T)objectValue; - // return value; - // } - //} - private static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) { var objectValue = defaultValueCalculation(); diff --git a/src/System.CommandLine.Tests/ParseResultValueTests.cs b/src/System.CommandLine.Tests/ParseResultValueTests.cs new file mode 100644 index 0000000000..63b51015f6 --- /dev/null +++ b/src/System.CommandLine.Tests/ParseResultValueTests.cs @@ -0,0 +1,55 @@ +// 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.Parsing; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace System.CommandLine.Tests; + +public class ParseResultValueTests +{ + [Fact] + public void Symbol_found_by_name() + { + var option1 = new CliOption("--opt1"); + var option2 = new CliOption("--opt2"); + + var rootCommand = new CliRootCommand + { + option1, + option2 + }; + + var parseResult = CliParser.Parse(rootCommand, "--opt1 Kirk"); + + var symbol1 = parseResult.GetSymbolByName("--opt1"); + var symbol2 = parseResult.GetSymbolByName("--opt2"); + symbol1.Should().Be(option1); + symbol2.Should().Be(option2); + } + + [Fact] + public void Nearest_symbol_found_when_multiple() + { + var option1 = new CliOption("--opt1", "-1"); + var option2 = new CliOption("--opt1", "-2"); + + var command = new CliCommand("subcommand") + { + option2 + }; + + var rootCommand = new CliRootCommand + { + command, + option1 + }; + + var parseResult = CliParser.Parse(rootCommand, "subcommand --opt2 Spock"); + + var symbol = parseResult.GetSymbolByName("--opt1"); + symbol.Should().Be(option2); + } +} \ No newline at end of file diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 72280dd9c0..9e23883749 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -1256,12 +1256,12 @@ public void Single_option_arguments_that_match_option_aliases_are_parsed_correct } [Theory] - [InlineData("-x -y")] - [InlineData("-x true -y")] - [InlineData("-x:true -y")] - [InlineData("-x=true -y")] + [InlineData("-x -y")] // + [InlineData("-x true -y")] // + [InlineData("-x:true -y")] // + [InlineData("-x=true -y")] // [InlineData("-x -y true")] - [InlineData("-x true -y true")] + [InlineData("-x true -y true")] // [InlineData("-x:true -y:true")] [InlineData("-x=true -y:true")] public void Boolean_options_are_not_greedy(string commandLine) @@ -1874,6 +1874,28 @@ public void Location_offset_correct_when_colon_or_equal_used() result2.Locations.Single().Should().Be(expectedLocation2); } + [Fact] + public void Locations_correct_for_collection() + { + var option1 = new CliOption("--opt1"); + option1.AllowMultipleArgumentsPerToken = true; + var rootCommand = new CliRootCommand + { + option1 + }; + var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedLocation1 = new Location("Kirk", Location.User, 2, expectedOuterLocation); + var expectedLocation2 = new Location("Spock", Location.User, 3, expectedOuterLocation); + var expectedLocation3 = new Location("Uhura", Location.User, 4, expectedOuterLocation); + + var parseResult = CliParser.Parse(rootCommand, "subcommand --opt1 Kirk Spock Uhura"); + + var commandResult = parseResult.CommandResult; + parseResult.GetValue(option1); + var result1 = parseResult.GetValueResult(option1); + result1.Locations.Should().BeEquivalentTo([expectedLocation1, expectedLocation2, expectedLocation3]); + } + [Fact] public void ParseResult_contains_argument_ValueResults() { diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index bdf2cd711e..a03838fbd6 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -20,9 +20,10 @@ + - + diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 7c4e1a437f..1f672ec35e 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -15,6 +15,7 @@ namespace System.CommandLine public sealed class ParseResult { private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); + private Dictionary symbolByName = null; private readonly CommandResult _rootCommandResult; // TODO: unmatched tokens, invocation, completion @@ -57,6 +58,7 @@ internal ParseResult( */ /* // skip the root command when populating Tokens property + /* if (tokens.Count > 1) { // Since TokenizeResult.Tokens is not public and not used anywhere after the parsing, @@ -79,6 +81,51 @@ internal ParseResult( Errors = errors is not null ? errors : Array.Empty(); } + private Dictionary PopulateSymbolByName() + { + var commands = GetSelfAndAncestors(CommandResult); + var ret = new Dictionary { }; + + foreach (var command in commands) + { + if (command.HasOptions) + { + foreach (var option in command.Options) + { + ret[option.Name] = option; + } + } + if (command.HasArguments) + { + foreach (var argument in command.Arguments) + { + ret[argument.Name] = argument; + } + } + } + return ret; + + static IEnumerable GetSelfAndAncestors(CommandResult commandResult) + { + var ret = new List { commandResult.Command }; + while (commandResult.Parent is CommandResult parent) + { + commandResult = parent; + ret.Add(parent.Command); + } + ret.Reverse(); + return ret; + } + } + + public CliSymbol GetSymbolByName(string name) + { + symbolByName ??= PopulateSymbolByName(); + return symbolByName.TryGetValue(name, out var symbol) + ? symbol + : throw new ArgumentException($"No symbol result found with name \"{name}\"."); + } + // TODO: check that constructing empty ParseResult directly is correct /* internal static ParseResult Empty() => new CliRootCommand().Parse(Array.Empty()); @@ -143,7 +190,7 @@ CommandLineText is null /// The argument for which to get a value. /// The parsed value or a configured default. public T? GetValue(CliArgument argument) - => RootCommandResult.GetValue(argument); + => GetValueInternal(argument); /// /// Gets the parsed or default value for the specified option. @@ -151,7 +198,12 @@ CommandLineText is null /// The option for which to get a value. /// The parsed value or a configured default. public T? GetValue(CliOption option) - => RootCommandResult.GetValue(option); + => GetValueInternal(option); + + private T? GetValueInternal(CliSymbol symbol) + => valueResultDictionary.TryGetValue(symbol, out var result) + ? (T?)result.Value + : default; /// /// Gets the parsed or default value for the specified symbol name, in the context of parsed command (not entire symbol tree). @@ -170,9 +222,13 @@ CommandLineText is null public override string ToString() => ParseDiagramAction.Diagram(this).ToString(); */ - public ValueResult? GetValueResult(CliSymbol symbol) - => valueResultDictionary.TryGetValue(symbol, out var result) - ? result + public ValueResult? GetValueResult(CliOption option) + => GetValueResultInternal(option); + public ValueResult? GetValueResult(CliArgument argument) + => GetValueResultInternal(argument); + private ValueResult? GetValueResultInternal(CliSymbol symbol) + => valueResultDictionary.TryGetValue(symbol, out var result) + ? result : null; /// diff --git a/src/System.CommandLine/Parsing/CommandValueResult.cs b/src/System.CommandLine/Parsing/CommandValueResult.cs index 01ea7085d2..4225fac089 100644 --- a/src/System.CommandLine/Parsing/CommandValueResult.cs +++ b/src/System.CommandLine/Parsing/CommandValueResult.cs @@ -7,5 +7,13 @@ namespace System.CommandLine.Parsing; public class CommandValueResult { + public CommandValueResult(CliCommand command, CommandValueResult parent) + { + Command = command; + Parent = parent; + } public IEnumerable ValueResults { get; } = new List(); + public CliCommand Command { get; } + public CommandValueResult Parent { get; } + } diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/OptionResult.cs index ccba50a386..794cf1d0b9 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/OptionResult.cs @@ -34,10 +34,17 @@ public ValueResult ValueResult { // This is not lazy on the assumption that almost everything the user enters will be used, and ArgumentResult is no longer used for defaults // TODO: Make sure errors are added - var conversionValue = ArgumentConversionResult.Value; + var conversionResult = ArgumentConversionResult + .ConvertIfNeeded(Option.Argument.ValueType); + var conversionValue = conversionResult.Result switch + { + ArgumentConversionResultType.Successful => conversionResult.Value!, + ArgumentConversionResultType.NoArgument => default!, + _ => default // This is an error condition, and is handled below + }; var locations = Tokens.Select(token => token.Location).ToArray(); //TODO: Remove this wrapper later - _valueResult = new ValueResult(Option, conversionValue, locations, ArgumentResult.GetValueResultOutcome(ArgumentConversionResult?.Result)); // null is temporary here + _valueResult = new ValueResult(Option, conversionValue, locations, ArgumentResult.GetValueResultOutcome(ArgumentConversionResult?.Result), conversionResult.ErrorMessage); } return _valueResult; } @@ -54,20 +61,20 @@ public ValueResult ValueResult /// Implicit results commonly result from options having a default value. public bool Implicit => IdentifierToken is null || IdentifierToken.Implicit; -// TODO: make internal because exposes tokens + // TODO: make internal because exposes tokens /// /// The token that was parsed to specify the option. /// /// An identifier token is a token that matches either the option's name or one of its aliases. internal CliToken? IdentifierToken { get; } -// TODO: do we even need IdentifierTokenCount -/* - /// - /// The number of occurrences of an identifier token matching the option. - /// - public int IdentifierTokenCount { get; internal set; } -*/ + // TODO: do we even need IdentifierTokenCount + /* + /// + /// The number of occurrences of an identifier token matching the option. + /// + public int IdentifierTokenCount { get; internal set; } + */ /// public override string ToString() => $"{nameof(OptionResult)}: {IdentifierToken?.Value ?? Option.Name} {string.Join(" ", Tokens.Select(t => t.Value))}"; diff --git a/src/System.CommandLine/Parsing/ValueResult.cs b/src/System.CommandLine/Parsing/ValueResult.cs index 8899576224..93d3d319dd 100644 --- a/src/System.CommandLine/Parsing/ValueResult.cs +++ b/src/System.CommandLine/Parsing/ValueResult.cs @@ -36,9 +36,21 @@ internal ValueResult( public string? Error { get; } + public IEnumerable TextForDisplay() + { + throw new NotImplementedException(); + } + + public IEnumerable TextForCommandReconstruction() + { + throw new NotImplementedException(); + } + public override string ToString() - => $"{nameof(ValueResult)} ({FormatOutcomeMessage()}) {ValueSymbol?.Name}"; + //=> $"{nameof(ValueResult)} ({FormatOutcomeMessage()}) {ValueSymbol?.Name}"; + => $"{nameof(ArgumentResult)} {ValueSymbol.Name}: {string.Join(" ", TextForDisplay())}"; + // TODO: This definitely feels like the wrong place for this, (Some completion stuff was stripped out. This was a private method in ArgumentConversionResult private string FormatOutcomeMessage() => ValueSymbol switch From abb1226544bec8fc075c2ebb81b685c2d9f8ba55 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 28 Apr 2024 08:36:58 -0400 Subject: [PATCH 075/150] Add generic annotation accessor This was later moved to its own file --- .../Annotations/AnnotationAccessor.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs index ec3b45e3c4..5e8304c871 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs @@ -36,3 +36,21 @@ public struct AnnotationAccessor(CliSubsystem owner, AnnotationIdTrue if the value was found, false otherwise. public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, Id, out value); } + +/// +/// Allows associating an annotation with a . The annotation will be stored by the accessor's owner . +/// +public struct ValueAnnotationAccessor(CliSubsystem owner, AnnotationId id) +{ + /// + /// The ID of the annotation + /// + public AnnotationId Id { get; } + public readonly void Set(CliOption symbol, TSymbolValue value) + where TSymbolValue : TValue + => owner.SetAnnotation(symbol, id, value); + public readonly void Set(CliArgument symbol, TSymbolValue value) + where TSymbolValue : TValue + => owner.SetAnnotation(symbol, id, value); + public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, id, out value); +} From 0933deb00ac22a9e4e46490d9b4b23eba2b59357 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 28 Apr 2024 08:37:40 -0400 Subject: [PATCH 076/150] Work on tests and removed Set/GetDefaultValue --- .../ValueSubsystemTests.cs | 29 ++++++++++--------- .../ValueSubsystem.cs | 14 ++++----- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index f5b57d4114..58c4207de6 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -30,28 +30,31 @@ public void Value_is_always_activated() isActive.Should().BeTrue(); } - [Fact] + [Fact(Skip ="WIP")] public void ValueSubsystem_returns_values_that_are_entered() { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var pipeline = Pipeline.Create(); + CliOption option1 = new CliOption("--intValue"); CliRootCommand rootCommand = [ new CliCommand("x") { - new CliOption("--intValue"), - new CliOption("--stringValue"), - new CliOption("--boolValue") + option1 }]; var configuration = new CliConfiguration(rootCommand); - var subsystem = new ValueSubsystem(); const int expected1 = 42; - const string expected2 = "43"; - var input = $"x --intValue {expected1} --stringValue \"{expected2}\" --boolValue"; - var args = CliParser.SplitCommandLine(input).ToList(); + var input = $"x --intValue {expected1}"; - Subsystem.Initialize(subsystem, configuration, args); - var parseResult = CliParser.Parse(rootCommand, input, configuration); + pipeline.Parse(configuration, input); + pipeline.Execute(configuration, input, consoleHack); + + pipeline.Value.GetValue(option1).Should().Be(expected1); + } + + + [Fact(Skip = "WIP")] + public void ValueSubsystem_returns_default_value_when_no_value_is_entered() + { - parseResult.GetValue("--intValue").Should().Be(expected1); - parseResult.GetValue("--stringValue").Should().Be(expected2); - parseResult.GetValue("--boolValue").Should().Be(true); } } diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index ff5020028b..f55436a5dd 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -18,13 +18,13 @@ public ValueSubsystem(IAnnotationProvider? annotationProvider = null) : base(ValueAnnotations.Prefix, SubsystemKind.Version, annotationProvider) { } - internal void SetDefaultValue(CliSymbol symbol, object? defaultValue) - => SetAnnotation(symbol, ValueAnnotations.DefaultValue, defaultValue); - internal object? GetDefaultValue(CliSymbol symbol) - => TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var defaultValue) - ? defaultValue - : ""; - internal bool TryGetDefaultValue(CliSymbol symbol, out T? defaultValue) + //internal void SetDefaultValue(CliSymbol symbol, object? defaultValue) + // => SetAnnotation(symbol, ValueAnnotations.DefaultValue, defaultValue); + //internal object? GetDefaultValue(CliSymbol symbol) + // => TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var defaultValue) + // ? defaultValue + // : ""; + private bool TryGetDefaultValue(CliSymbol symbol, out T? defaultValue) { if (TryGetAnnotation(symbol, ValueAnnotations.Value, out var objectValue)) { From 5398b0952736a29a49aa9007bfb945f7fa74a5a4 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 28 Apr 2024 14:47:13 -0400 Subject: [PATCH 077/150] Made SymbolResult internal --- .../Directives/DiagramSubsystem.cs | 5 +++-- src/System.CommandLine/Parsing/ParseError.cs | 17 ++++++++++++++++- src/System.CommandLine/Parsing/SymbolResult.cs | 6 +++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 5168822b76..90cc244382 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -57,6 +57,7 @@ internal static StringBuilder Diagram(ParseResult parseResult) return builder; } + /* private static void Diagram( StringBuilder builder, SymbolResult symbolResult, @@ -66,7 +67,7 @@ private static void Diagram( { builder.Append('!'); } - + */ // TODO: Directives /* switch (symbolResult) @@ -176,6 +177,6 @@ private static void Diagram( } } } -*/ } +*/ } diff --git a/src/System.CommandLine/Parsing/ParseError.cs b/src/System.CommandLine/Parsing/ParseError.cs index db3d7e73ab..812a2b209c 100644 --- a/src/System.CommandLine/Parsing/ParseError.cs +++ b/src/System.CommandLine/Parsing/ParseError.cs @@ -10,7 +10,7 @@ public sealed class ParseError { // TODO: add position // TODO: reevaluate whether we should be exposing a SymbolResult here - public ParseError( + internal ParseError( string message, SymbolResult? symbolResult = null) { @@ -20,7 +20,20 @@ public ParseError( } Message = message; + /* SymbolResult = symbolResult; + */ + } + + public ParseError( + string message) + { + if (string.IsNullOrWhiteSpace(message)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(message)); + } + + Message = message; } /// @@ -28,10 +41,12 @@ public ParseError( /// public string Message { get; } + /* Consider how results are attached to errors now that we have ValueResult and CommandValueResult. Should there be a common base? /// /// The symbol result detailing the symbol that failed to parse and the tokens involved. /// public SymbolResult? SymbolResult { get; } + */ /// public override string ToString() => Message; diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/SymbolResult.cs index 38da2e21c1..908e2afb05 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/SymbolResult.cs @@ -8,7 +8,7 @@ namespace System.CommandLine.Parsing /// /// A result produced during parsing for a specific symbol. /// - public abstract class SymbolResult + internal abstract class SymbolResult { // TODO: make this a property and protected if possible internal readonly SymbolResultTree SymbolResultTree; @@ -72,12 +72,14 @@ public IEnumerable Errors /// An argument result if the argument was matched by the parser or has a default value; otherwise, null. internal ArgumentResult? GetResult(CliArgument argument) => SymbolResultTree.GetResult(argument); + /* Not used /// /// Finds a result for the specific command anywhere in the parse tree, including parent and child symbol results. /// /// The command for which to find a result. /// An command result if the command was matched by the parser; otherwise, null. internal CommandResult? GetResult(CliCommand command) => SymbolResultTree.GetResult(command); + */ /// /// Finds a result for the specific option anywhere in the parse tree, including parent and child symbol results. @@ -103,6 +105,7 @@ public IEnumerable Errors public SymbolResult? GetResult(string name) => SymbolResultTree.GetResult(name); + /* Not used /// public T? GetValue(CliArgument argument) { @@ -126,6 +129,7 @@ public IEnumerable Errors return CliArgument.CreateDefaultValue(); } + */ /// /// Gets the value for a symbol having the specified name anywhere in the parse tree. From ffca21e0c2dfdc5cf468053a9ea1c9b1f4d88ba6 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 28 Apr 2024 18:29:51 -0400 Subject: [PATCH 078/150] Replacing SymbolByName and changes to GetValue Design previously had SymbolByName and GetValue on SymbolResultTree which is a parse time only concern. These were moved to ParseResult for accessibility outside parsing. --- .../ValueSubsystemTests.cs | 2 +- src/System.CommandLine.Tests/ParserTests.cs | 4 +- src/System.CommandLine/CliCommand.cs | 1 + src/System.CommandLine/ParseResult.cs | 429 +++++++++--------- .../Parsing/ParseOperation.cs | 2 +- .../Parsing/SymbolLookupByName.cs | 148 ++++++ .../Parsing/SymbolResult.cs | 4 +- .../Parsing/SymbolResultTree.cs | 63 +-- .../System.CommandLine.csproj | 1 + 9 files changed, 410 insertions(+), 244 deletions(-) create mode 100644 src/System.CommandLine/Parsing/SymbolLookupByName.cs diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index 58c4207de6..c70b69cc28 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -30,7 +30,7 @@ public void Value_is_always_activated() isActive.Should().BeTrue(); } - [Fact(Skip ="WIP")] + [Fact(Skip = "WIP")] public void ValueSubsystem_returns_values_that_are_entered() { var consoleHack = new ConsoleHack().RedirectToBuffer(true); diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 9e23883749..8ded430750 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -1501,7 +1501,7 @@ public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_tha new CliToken("5", CliTokenType.Argument, argument, dummyLocation)); } - [Fact] + [Fact(Skip ="Waiting for CliError work")] public void When_command_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() { var command = new CliCommand("the-command") @@ -1590,7 +1590,7 @@ public void Option_argument_arity_can_be_a_range_with_a_lower_bound_greater_than new CliToken("5", CliTokenType.Argument, default, dummyLocation)); } - [Fact] + [Fact(Skip = "Waiting for CliError work")] public void When_option_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() { var option = new CliOption("-x") diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index 5749cae3b3..b4e01e3909 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -75,6 +75,7 @@ public IEnumerable Children /// /// Represents all of the options for the command, inherited options that have been applied to any of the command's ancestors. /// + // TODO: Consider value of lazy here. It sets up a desire to use awkward approach (HasOptions) for a perf win. Applies to Options and Subcommands also. public IList Options => _options ??= new (this); internal bool HasOptions => _options?.Count > 0; diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 1f672ec35e..2ce4f4eca9 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -16,46 +16,47 @@ public sealed class ParseResult { private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); private Dictionary symbolByName = null; + private SymbolLookupByName symbolLookupByName = null; private readonly CommandResult _rootCommandResult; -// TODO: unmatched tokens, invocation, completion -/* - private readonly IReadOnlyList _unmatchedTokens; - private CompletionContext? _completionContext; - private readonly CliAction? _action; - private readonly List? _preActions; -*/ + // TODO: unmatched tokens, invocation, completion + /* + private readonly IReadOnlyList _unmatchedTokens; + private CompletionContext? _completionContext; + private readonly CliAction? _action; + private readonly List? _preActions; + */ internal ParseResult( CliConfiguration configuration, -// TODO: determine how rootCommandResult and commandResult differ + // TODO: determine how rootCommandResult and commandResult differ CommandResult rootCommandResult, CommandResult commandResult, - Dictionary valueResults, + SymbolResultTree symbolResultTree, /* List tokens, */ -// TODO: unmatched tokens -// List? unmatchedTokens, + // TODO: unmatched tokens + // List? unmatchedTokens, List? errors, -// TODO: commandLineText should be string array + // TODO: commandLineText should be string array string? commandLineText = null //, -// TODO: invocation -/* - CliAction? action = null, - List? preActions = null) -*/ + // TODO: invocation + /* + CliAction? action = null, + List? preActions = null) + */ ) { Configuration = configuration; _rootCommandResult = rootCommandResult; CommandResult = commandResult; - valueResultDictionary = valueResults; + valueResultDictionary = symbolResultTree.BuildValueResultDictionary(); // TODO: invocation -/* - _action = action; - _preActions = preActions; -*/ + /* + _action = action; + _preActions = preActions; + */ /* // skip the root command when populating Tokens property /* @@ -75,61 +76,62 @@ internal ParseResult( CommandLineText = commandLineText; -// TODO: unmatched tokens -// _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; + // TODO: unmatched tokens + // _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; Errors = errors is not null ? errors : Array.Empty(); } - private Dictionary PopulateSymbolByName() + //private Dictionary PopulateSymbolByName() + //{ + // var commands = GetSelfAndAncestors(CommandResult); + // var ret = new Dictionary { }; + + // foreach (var command in commands) + // { + // if (command.HasOptions) + // { + // foreach (var option in command.Options) + // { + // ret[option.Name] = option; + // } + // } + // if (command.HasArguments) + // { + // foreach (var argument in command.Arguments) + // { + // ret[argument.Name] = argument; + // } + // } + // } + // return ret; + + // static IEnumerable GetSelfAndAncestors(CommandResult commandResult) + // { + // var ret = new List { commandResult.Command }; + // while (commandResult.Parent is CommandResult parent) + // { + // commandResult = parent; + // ret.Add(parent.Command); + // } + // ret.Reverse(); + // return ret; + // } + //} + + public CliSymbol GetSymbolByName(string name, bool valuesOnly = false) { - var commands = GetSelfAndAncestors(CommandResult); - var ret = new Dictionary { }; - foreach (var command in commands) - { - if (command.HasOptions) - { - foreach (var option in command.Options) - { - ret[option.Name] = option; - } - } - if (command.HasArguments) - { - foreach (var argument in command.Arguments) - { - ret[argument.Name] = argument; - } - } - } - return ret; - - static IEnumerable GetSelfAndAncestors(CommandResult commandResult) - { - var ret = new List { commandResult.Command }; - while (commandResult.Parent is CommandResult parent) - { - commandResult = parent; - ret.Add(parent.Command); - } - ret.Reverse(); - return ret; - } - } - - public CliSymbol GetSymbolByName(string name) - { - symbolByName ??= PopulateSymbolByName(); - return symbolByName.TryGetValue(name, out var symbol) + symbolLookupByName ??= new SymbolLookupByName(this); + return symbolLookupByName.TryGetSymbol(name, out var symbol, valuesOnly: valuesOnly) ? symbol : throw new ArgumentException($"No symbol result found with name \"{name}\"."); } -// TODO: check that constructing empty ParseResult directly is correct -/* - internal static ParseResult Empty() => new CliRootCommand().Parse(Array.Empty()); -*/ + // TODO: check that constructing empty ParseResult directly is correct + /* + internal static ParseResult Empty() => new CliRootCommand().Parse(Array.Empty()); + */ /// /// A result indicating the command specified in the command line input. @@ -214,7 +216,15 @@ CommandLineText is null /// Thrown when there was no symbol defined for given name for the parsed command. /// Thrown when parsed result can not be cast to . public T? GetValue(string name) - => RootCommandResult.GetValue(name); + { + var symbol = GetSymbolByName(name, valuesOnly: true); + return symbol switch + { + CliArgument argument => GetValue(argument), + CliOption option => GetValue(option), + _ => throw new InvalidOperationException("Unexpected symbol type") + }; + } // TODO: diagramming /* @@ -239,6 +249,7 @@ CommandLineText is null internal ArgumentResult? GetResult(CliArgument argument) => _rootCommandResult.GetResult(argument); + /* Not used /// /// Gets the result, if any, for the specified command. /// @@ -246,6 +257,7 @@ CommandLineText is null /// A result for the specified command, or if it was not provided. internal CommandResult? GetResult(CliCommand command) => _rootCommandResult.GetResult(command); + */ /// /// Gets the result, if any, for the specified option. @@ -255,15 +267,16 @@ CommandLineText is null internal OptionResult? GetResult(CliOption option) => _rootCommandResult.GetResult(option); -// TODO: Directives -/* - /// - /// Gets the result, if any, for the specified directive. - /// - /// The directive for which to find a result. - /// A result for the specified directive, or if it was not provided. - public DirectiveResult? GetResult(CliDirective directive) => _rootCommandResult.GetResult(directive); -*/ + // TODO: Directives + /* + /// + /// Gets the result, if any, for the specified directive. + /// + /// The directive for which to find a result. + /// A result for the specified directive, or if it was not provided. + public DirectiveResult? GetResult(CliDirective directive) => _rootCommandResult.GetResult(directive); + */ + /* Replaced with GetValueResult /// /// Gets the result, if any, for the specified symbol. /// @@ -271,170 +284,170 @@ CommandLineText is null /// A result for the specified symbol, or if it was not provided and no default was configured. public SymbolResult? GetResult(CliSymbol symbol) => _rootCommandResult.SymbolResultTree.TryGetValue(symbol, out SymbolResult? result) ? result : null; + */ + // TODO: completion, invocation + /* + /// + /// Gets completions based on a given parse result. + /// + /// The position at which completions are requested. + /// A set of completions for completion. + public IEnumerable GetCompletions( + int? position = null) + { + SymbolResult currentSymbolResult = SymbolToComplete(position); -// TODO: completion, invocation -/* - /// - /// Gets completions based on a given parse result. - /// - /// The position at which completions are requested. - /// A set of completions for completion. - public IEnumerable GetCompletions( - int? position = null) - { - SymbolResult currentSymbolResult = SymbolToComplete(position); - - CliSymbol currentSymbol = currentSymbolResult switch - { - ArgumentResult argumentResult => argumentResult.Argument, - OptionResult optionResult => optionResult.Option, - DirectiveResult directiveResult => directiveResult.Directive, - _ => ((CommandResult)currentSymbolResult).Command - }; - - var context = GetCompletionContext(); - - if (position is not null && - context is TextCompletionContext tcc) - { - context = tcc.AtCursorPosition(position.Value); - } + CliSymbol currentSymbol = currentSymbolResult switch + { + ArgumentResult argumentResult => argumentResult.Argument, + OptionResult optionResult => optionResult.Option, + DirectiveResult directiveResult => directiveResult.Directive, + _ => ((CommandResult)currentSymbolResult).Command + }; - var completions = currentSymbol.GetCompletions(context); + var context = GetCompletionContext(); - string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult - ? OptionsWithArgumentLimitReached(commandResult) - : Array.Empty(); + if (position is not null && + context is TextCompletionContext tcc) + { + context = tcc.AtCursorPosition(position.Value); + } - completions = - completions.Where(item => optionsWithArgumentLimitReached.All(s => s != item.Label)); + var completions = currentSymbol.GetCompletions(context); - return completions; + string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult + ? OptionsWithArgumentLimitReached(commandResult) + : Array.Empty(); - static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => - commandResult - .Children - .OfType() - .Where(c => c.IsArgumentLimitReached) - .Select(o => o.Option) - .SelectMany(c => new[] { c.Name }.Concat(c.Aliases)) - .ToArray(); - } + completions = + completions.Where(item => optionsWithArgumentLimitReached.All(s => s != item.Label)); - /// - /// Invokes the appropriate command handler for a parsed command line input. - /// - /// A token that can be used to cancel an invocation. - /// A task whose result can be used as a process exit code. - public Task InvokeAsync(CancellationToken cancellationToken = default) - => InvocationPipeline.InvokeAsync(this, cancellationToken); + return completions; - /// - /// Invokes the appropriate command handler for a parsed command line input. - /// - /// A value that can be used as a process exit code. - public int Invoke() - { - var useAsync = false; + static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => + commandResult + .Children + .OfType() + .Where(c => c.IsArgumentLimitReached) + .Select(o => o.Option) + .SelectMany(c => new[] { c.Name }.Concat(c.Aliases)) + .ToArray(); + } - if (Action is AsynchronousCliAction) - { - useAsync = true; - } - else if (PreActions is not null) - { - for (var i = 0; i < PreActions.Count; i++) + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// A token that can be used to cancel an invocation. + /// A task whose result can be used as a process exit code. + public Task InvokeAsync(CancellationToken cancellationToken = default) + => InvocationPipeline.InvokeAsync(this, cancellationToken); + + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// A value that can be used as a process exit code. + public int Invoke() { - var action = PreActions[i]; - if (action is AsynchronousCliAction) + var useAsync = false; + + if (Action is AsynchronousCliAction) { useAsync = true; - break; } - } - } + else if (PreActions is not null) + { + for (var i = 0; i < PreActions.Count; i++) + { + var action = PreActions[i]; + if (action is AsynchronousCliAction) + { + useAsync = true; + break; + } + } + } - if (useAsync) - { - return InvocationPipeline.InvokeAsync(this, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); - } - else - { - return InvocationPipeline.Invoke(this); - } - } + if (useAsync) + { + return InvocationPipeline.InvokeAsync(this, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + } + else + { + return InvocationPipeline.Invoke(this); + } + } - /// - /// Gets the for parsed result. The handler represents the action - /// that will be performed when the parse result is invoked. - /// - public CliAction? Action => _action ?? CommandResult.Command.Action; + /// + /// Gets the for parsed result. The handler represents the action + /// that will be performed when the parse result is invoked. + /// + public CliAction? Action => _action ?? CommandResult.Command.Action; - internal IReadOnlyList? PreActions => _preActions; + internal IReadOnlyList? PreActions => _preActions; - private SymbolResult SymbolToComplete(int? position = null) - { - var commandResult = CommandResult; + private SymbolResult SymbolToComplete(int? position = null) + { + var commandResult = CommandResult; - var allSymbolResultsForCompletion = AllSymbolResultsForCompletion(); + var allSymbolResultsForCompletion = AllSymbolResultsForCompletion(); - var currentSymbol = allSymbolResultsForCompletion.Last(); + var currentSymbol = allSymbolResultsForCompletion.Last(); - return currentSymbol; + return currentSymbol; - IEnumerable AllSymbolResultsForCompletion() - { - foreach (var item in commandResult.AllSymbolResults()) - { - if (item is CommandResult command) + IEnumerable AllSymbolResultsForCompletion() { - yield return command; + foreach (var item in commandResult.AllSymbolResults()) + { + if (item is CommandResult command) + { + yield return command; + } + else if (item is OptionResult option) + { + if (WillAcceptAnArgument(this, position, option)) + { + yield return option; + } + } + } } - else if (item is OptionResult option) + + static bool WillAcceptAnArgument( + ParseResult parseResult, + int? position, + OptionResult optionResult) { - if (WillAcceptAnArgument(this, position, option)) + if (optionResult.Implicit) { - yield return option; + return false; } - } - } - } - static bool WillAcceptAnArgument( - ParseResult parseResult, - int? position, - OptionResult optionResult) - { - if (optionResult.Implicit) - { - return false; - } + if (!optionResult.IsArgumentLimitReached) + { + return true; + } - if (!optionResult.IsArgumentLimitReached) - { - return true; - } + var completionContext = parseResult.GetCompletionContext(); - var completionContext = parseResult.GetCompletionContext(); + if (completionContext is TextCompletionContext textCompletionContext) + { + if (position.HasValue) + { + textCompletionContext = textCompletionContext.AtCursorPosition(position.Value); + } - if (completionContext is TextCompletionContext textCompletionContext) - { - if (position.HasValue) - { - textCompletionContext = textCompletionContext.AtCursorPosition(position.Value); - } + if (textCompletionContext.WordToComplete.Length > 0) + { + var tokenToComplete = parseResult.Tokens.Last(t => t.Value == textCompletionContext.WordToComplete); - if (textCompletionContext.WordToComplete.Length > 0) - { - var tokenToComplete = parseResult.Tokens.Last(t => t.Value == textCompletionContext.WordToComplete); + return optionResult.Tokens.Contains(tokenToComplete); + } + } - return optionResult.Tokens.Contains(tokenToComplete); + return !optionResult.IsArgumentLimitReached; } } - - return !optionResult.IsArgumentLimitReached; - } - } - */ + */ } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index d78a0e55e8..8e972d2728 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -85,7 +85,7 @@ internal ParseResult Parse() _configuration, _rootCommandResult, _innermostCommandResult, - _rootCommandResult.SymbolResultTree.GetValueResultDictionary(), + _rootCommandResult.SymbolResultTree, /* _tokens, */ diff --git a/src/System.CommandLine/Parsing/SymbolLookupByName.cs b/src/System.CommandLine/Parsing/SymbolLookupByName.cs new file mode 100644 index 0000000000..2e4b65e901 --- /dev/null +++ b/src/System.CommandLine/Parsing/SymbolLookupByName.cs @@ -0,0 +1,148 @@ +// 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.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace System.CommandLine.Parsing; + +// Performance note: Special cases might result in the previous single dictionary being faster, but it did not +// give correct results for many CLIs, and also, it built a dictionary for the full CLI tree, rather than just the +// current commands and its ancestors, so in many cases, this will be faster. +// +// Most importantly, that approach fails for options, like the previous global options, that appear on multiple +// commands, since we are now explicitly putting them on all commands. + +public class SymbolLookupByName +{ + private class CommandCache(CliCommand command) + { + public CliCommand Command { get; } = command; + public Dictionary SymbolsByName { get; } = new(); + } + + private List cache; + + public SymbolLookupByName(ParseResult parseResult) + => cache = BuildCache(parseResult); + + private List BuildCache(ParseResult parseResult) + { + if (cache is not null) + { + return cache; + } + cache = []; + var commandResult = parseResult.CommandResult; + while (commandResult is not null) + { + var command = commandResult.Command; + if (TryGetCommandCache(command, out var _)) + { + throw new InvalidOperationException("Command hierarchy appears to be recursive."); + } + var commandCache = new CommandCache(command); + cache.Add(commandCache); + + AddSymbolsToCache(commandCache, command.Options, command); + AddSymbolsToCache(commandCache, command.Arguments, command); + AddSymbolsToCache(commandCache, command.Subcommands, command); + commandResult = (CommandResult?)commandResult.Parent; + } + + return cache; + + static void AddSymbolsToCache(CommandCache CommandCache, IEnumerable symbols, CliCommand command) + { + foreach (var symbol in symbols) + { + if (CommandCache.SymbolsByName.ContainsKey(symbol.Name)) + { + throw new InvalidOperationException($"Command {command.Name} has more than one child named \"{symbol.Name}\"."); + } + CommandCache.SymbolsByName.Add(symbol.Name, symbol); + } + } + } + + private bool TryGetCommandCache(CliCommand command, [NotNullWhen(true)] out CommandCache? commandCache) + { + var candidates = cache.Where(x => x.Command == command); + if (candidates.Any()) + { + commandCache = candidates.Single(); // multiples are a failure in construction + return true; + } + commandCache = null; + return false; + } + + private bool TryGetSymbolAndParentInternal(string name, + [NotNullWhen(true)] out CliSymbol? symbol, + [NotNullWhen(true)] out CliCommand? parent, + [NotNullWhen(false)] out string? errorMessage, + CliCommand? startCommand, + bool skipAncestors, + bool valuesOnly) + { + startCommand ??= cache.First().Command; // The construction of the dictionary makes this the parseResult.CommandResult - current command + var commandCaches = GetCommandCachesToUse(startCommand); + if (commandCaches is null || !commandCaches.Any()) + { + errorMessage = $"Requested command {startCommand.Name} is not in the results."; + symbol = null; + parent = null; + return false; + } + + foreach (var commandCache in commandCaches) + { + if (commandCache.SymbolsByName.TryGetValue(name, out symbol)) + { + if (symbol is not null && (!valuesOnly || (symbol is CliArgument or CliOption))) + { + parent = commandCache.Command; + errorMessage = null; + return true; + } + } + + if (skipAncestors) + { + break; + } + } + + errorMessage = $"Requested symbol {name} was not found."; + symbol = null; + parent = null; + return false; + } + + public (CliSymbol symbol, CliCommand parent) GetSymbolAndParent(string name, CliCommand? startCommand = null, bool skipAncestors = false, bool valuesOnly = false) + => TryGetSymbolAndParentInternal(name, out var symbol, out var parent, out var errorMessage, startCommand, skipAncestors, valuesOnly) + ? (symbol, parent) + : throw new InvalidOperationException(errorMessage); + + public bool TryGetSymbol(string name, out CliSymbol symbol, CliCommand? startCommand = null, bool skipAncestors = false, bool valuesOnly = false) + => TryGetSymbolAndParentInternal(name, out symbol, out var _, out var _, startCommand, skipAncestors, valuesOnly); + + + private IEnumerable? GetCommandCachesToUse(CliCommand currentCommand) + { + if (cache[0].Command == currentCommand) + { + return cache; + } + for (int i = 1; i < cache.Count; i++) // we tested for 0 earlier + { + if (cache[i].Command == currentCommand) + { + return cache.Skip(i); + } + } + return null; + } +} diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/SymbolResult.cs index 908e2afb05..f32bec67c4 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/SymbolResult.cs @@ -97,6 +97,7 @@ public IEnumerable Errors /// A directive result if the directive was matched by the parser, null otherwise. public DirectiveResult? GetResult(CliDirective directive) => SymbolResultTree.GetResult(directive); */ + /* No longer used /// /// Finds a result for a symbol having the specified name anywhere in the parse tree. /// @@ -105,7 +106,6 @@ public IEnumerable Errors public SymbolResult? GetResult(string name) => SymbolResultTree.GetResult(name); - /* Not used /// public T? GetValue(CliArgument argument) { @@ -129,7 +129,6 @@ public IEnumerable Errors return CliArgument.CreateDefaultValue(); } - */ /// /// Gets the value for a symbol having the specified name anywhere in the parse tree. @@ -155,6 +154,7 @@ public IEnumerable Errors return CliArgument.CreateDefaultValue(); } + */ internal virtual bool UseDefaultValueFor(ArgumentResult argumentResult) => false; } diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 37319d604a..74e25451cd 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -10,15 +10,15 @@ internal sealed class SymbolResultTree : Dictionary { private readonly CliCommand _rootCommand; internal List? Errors; -// TODO: unmatched tokens -/* - internal List? UnmatchedTokens; -*/ + // TODO: unmatched tokens + /* + internal List? UnmatchedTokens; + */ // TODO: Looks like this is a SymboNode/linked list because a symbol may appear multiple // places in the tree and multiple symbols will have the same short name. The question is // whether creating the multiple node instances is faster than just using lists. Could well be. - private Dictionary? _symbolsByName; + //private Dictionary? _symbolsByName; internal SymbolResultTree( CliCommand rootCommand, List? tokenizeErrors) @@ -35,6 +35,7 @@ internal SymbolResultTree( } } } + internal int ErrorCount => Errors?.Count ?? 0; internal ArgumentResult? GetResult(CliArgument argument) @@ -46,11 +47,11 @@ internal SymbolResultTree( internal OptionResult? GetResult(CliOption option) => TryGetValue(option, out SymbolResult? result) ? (OptionResult)result : default; -//TODO: directives -/* - internal DirectiveResult? GetResult(CliDirective directive) - => TryGetValue(directive, out SymbolResult? result) ? (DirectiveResult)result : default; -*/ + //TODO: directives + /* + internal DirectiveResult? GetResult(CliDirective directive) + => TryGetValue(directive, out SymbolResult? result) ? (DirectiveResult)result : default; + */ // TODO: Determine how this is used. It appears to be O^n in the size of the tree and so if it is called multiple times, we should reconsider to avoid O^(N*M) internal IEnumerable GetChildren(SymbolResult parent) { @@ -67,11 +68,11 @@ internal IEnumerable GetChildren(SymbolResult parent) } } - internal Dictionary GetValueResultDictionary() + internal IReadOnlyDictionary BuildValueResultDictionary() { var dict = new Dictionary(); foreach (KeyValuePair pair in this) - { + { var result = pair.Value; if (result is OptionResult optionResult) { @@ -92,22 +93,23 @@ internal Dictionary GetValueResultDictionary() internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, CommandResult rootCommandResult) { -/* -// TODO: unmatched tokens - (UnmatchedTokens ??= new()).Add(token); - - if (commandResult.Command.TreatUnmatchedTokensAsErrors) - { - if (commandResult != rootCommandResult && !rootCommandResult.Command.TreatUnmatchedTokensAsErrors) - { - return; - } - -*/ + /* + // TODO: unmatched tokens + (UnmatchedTokens ??= new()).Add(token); + + if (commandResult.Command.TreatUnmatchedTokensAsErrors) + { + if (commandResult != rootCommandResult && !rootCommandResult.Command.TreatUnmatchedTokensAsErrors) + { + return; + } + + */ AddError(new ParseError(LocalizationResources.UnrecognizedCommandOrArgument(token.Value), commandResult)); // } } + /* No longer used public SymbolResult? GetResult(string name) { if (_symbolsByName is null) @@ -135,12 +137,12 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com return null; } -// TODO: symbolsbyname - this is inefficient -// results for some values may not be queried at all, dependent on other options -// so we could avoid using their value factories and adding them to the dictionary -// could we sort by name allowing us to do a binary search instead of allocating a dictionary? -// could we add codepaths that query for specific kinds of symbols so they don't have to search all symbols? -// Additional Note: Couldn't commands know their children, and thus this involves querying the active command, and possibly the parents + // TODO: symbolsbyname - this is inefficient + // results for some values may not be queried at all, dependent on other options + // so we could avoid using their value factories and adding them to the dictionary + // could we sort by name allowing us to do a binary search instead of allocating a dictionary? + // could we add codepaths that query for specific kinds of symbols so they don't have to search all symbols? + // Additional Note: Couldn't commands know their children, and thus this involves querying the active command, and possibly the parents private void PopulateSymbolsByName(CliCommand command) { if (command.HasArguments) @@ -192,5 +194,6 @@ void AddToSymbolsByName(CliSymbol symbol) } } } + */ } } \ No newline at end of file diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 6cd32ef4d8..9e45744e6b 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -28,6 +28,7 @@ + From 35f7ff1b9563bb780b7351f16e75fcc2696359de Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Wed, 1 May 2024 09:16:18 -0400 Subject: [PATCH 079/150] Updated Pipeline execution, bug fixes and cleanup Simple tests now pass. --- .../ValueSubsystemTests.cs | 44 +++++++- src/System.CommandLine.Subsystems/Pipeline.cs | 103 +++++------------- .../Subsystems/CliSubsystem.cs | 4 +- .../ValueSubsystem.cs | 19 ++-- src/System.CommandLine/Parsing/ValueResult.cs | 4 +- 5 files changed, 86 insertions(+), 88 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index c70b69cc28..b7b64d869f 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -30,11 +30,10 @@ public void Value_is_always_activated() isActive.Should().BeTrue(); } - [Fact(Skip = "WIP")] + [Fact] public void ValueSubsystem_returns_values_that_are_entered() { var consoleHack = new ConsoleHack().RedirectToBuffer(true); - var pipeline = Pipeline.Create(); CliOption option1 = new CliOption("--intValue"); CliRootCommand rootCommand = [ new CliCommand("x") @@ -42,19 +41,54 @@ public void ValueSubsystem_returns_values_that_are_entered() option1 }]; var configuration = new CliConfiguration(rootCommand); + var pipeline = Pipeline.CreateEmpty(); + pipeline.Value = new ValueSubsystem(); const int expected1 = 42; var input = $"x --intValue {expected1}"; - pipeline.Parse(configuration, input); + var parseResult = pipeline.Parse(configuration, input); // assigned for debugging pipeline.Execute(configuration, input, consoleHack); pipeline.Value.GetValue(option1).Should().Be(expected1); } - - [Fact(Skip = "WIP")] + [Fact] public void ValueSubsystem_returns_default_value_when_no_value_is_entered() { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + CliOption option1 = new CliOption("--intValue"); + CliRootCommand rootCommand = [option1]; + var configuration = new CliConfiguration(rootCommand); + var pipeline = Pipeline.CreateEmpty(); + pipeline.Value = new ValueSubsystem(); + pipeline.Value.DefaultValue.Set(option1, 43); + const int expected1 = 43; + var input = $""; + + var parseResult = pipeline.Parse(configuration, input); // assigned for debugging + pipeline.Execute(configuration, input, consoleHack); + + pipeline.Value.GetValue(option1).Should().Be(expected1); + } + + [Fact] + public void ValueSubsystem_returns_calculated_default_value_when_no_value_is_entered() + { + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + CliOption option1 = new CliOption("--intValue"); + CliRootCommand rootCommand = [option1]; + var configuration = new CliConfiguration(rootCommand); + var pipeline = Pipeline.CreateEmpty(); + pipeline.Value = new ValueSubsystem(); + var x = 42; + pipeline.Value.DefaultValueCalculation.Set(option1, () => x + 2); + const int expected1 = 44; + var input = $""; + + var parseResult = pipeline.Parse(configuration, input); // assigned for debugging + pipeline.Execute(configuration, input, consoleHack); + + pipeline.Value.GetValue(option1).Should().Be(expected1); } } diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index e3ca8e9bf3..d2be0b2dfd 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -4,11 +4,16 @@ using System.CommandLine.Directives; using System.CommandLine.Parsing; using System.CommandLine.Subsystems; +using System.Reflection.PortableExecutable; namespace System.CommandLine; public class Pipeline { + //TODO: When we allow adding subsystems, this code will change + private IEnumerable Subsystems + => [Help, Version, Completion, Diagram, Value, ErrorReporting]; + public static Pipeline Create(HelpSubsystem? help = null, VersionSubsystem? version = null, CompletionSubsystem? completion = null, @@ -63,61 +68,6 @@ public CliExit Execute(ParseResult parseResult, string rawInput, ConsoleHack? co return new CliExit(pipelineContext); } - protected virtual void InitializeHelp(InitializationContext context) - => Help?.Initialize(context); - - protected virtual void InitializeVersion(InitializationContext context) - => Version?.Initialize(context); - - protected virtual void InitializeCompletion(InitializationContext context) - => Completion?.Initialize(context); - - protected virtual void InitializeDiagram(InitializationContext context) - => Diagram?.Initialize(context); - - protected virtual void InitializeErrorReporting(InitializationContext context) - => ErrorReporting?.Initialize(context); - - protected virtual CliExit TearDownHelp(CliExit cliExit) - => Help is null - ? cliExit - : Help.TearDown(cliExit); - - protected virtual CliExit? TearDownVersion(CliExit cliExit) - => Version is null - ? cliExit - : Version.TearDown(cliExit); - - protected virtual CliExit TearDownCompletion(CliExit cliExit) - => Completion is null - ? cliExit - : Completion.TearDown(cliExit); - - protected virtual CliExit TearDownDiagram(CliExit cliExit) - => Diagram is null - ? cliExit - : Diagram.TearDown(cliExit); - - protected virtual CliExit TearDownErrorReporting(CliExit cliExit) - => ErrorReporting is null - ? cliExit - : ErrorReporting.TearDown(cliExit); - - protected virtual void ExecuteHelp(PipelineContext context) - => ExecuteIfNeeded(Help, context); - - protected virtual void ExecuteVersion(PipelineContext context) - => ExecuteIfNeeded(Version, context); - - protected virtual void ExecuteCompletion(PipelineContext context) - => ExecuteIfNeeded(Completion, context); - - protected virtual void ExecuteDiagram(PipelineContext context) - => ExecuteIfNeeded(Diagram, context); - - protected virtual void ExecuteErrorReporting(PipelineContext context) - => ExecuteIfNeeded(ErrorReporting, context); - // TODO: Consider whether this should be public. It would simplify testing, but would it do anything else // TODO: Confirm that it is OK for ConsoleHack to be unavailable in Initialize /// @@ -131,11 +81,13 @@ protected virtual void ExecuteErrorReporting(PipelineContext context) /// protected virtual void InitializeSubsystems(InitializationContext context) { - InitializeHelp(context); - InitializeVersion(context); - InitializeCompletion(context); - InitializeDiagram(context); - InitializeErrorReporting(context); + foreach (var subsystem in Subsystems) + { + if ( subsystem is not null) + { + subsystem.Initialize(context); + } + } } // TODO: Consider whether this should be public @@ -144,26 +96,31 @@ protected virtual void InitializeSubsystems(InitializationContext context) /// Perform any cleanup operations /// /// The context of the current execution - /// - /// Note to inheritors: The ordering of tear down should normally be in the reverse order than initializing - /// protected virtual CliExit TearDownSubsystems(CliExit cliExit) { - TearDownErrorReporting(cliExit); - TearDownDiagram(cliExit); - TearDownCompletion(cliExit); - TearDownVersion(cliExit); - TearDownHelp(cliExit); + // TODO: Work on this design as the last cliExit wins and they may not all be well behaved + var subsystems = Subsystems.Reverse(); + foreach (var subsystem in subsystems) + { + if (subsystem is not null) + { + cliExit = subsystem.TearDown(cliExit); + } + } return cliExit; } protected virtual void ExecuteSubsystems(PipelineContext pipelineContext) { - ExecuteHelp(pipelineContext); - ExecuteVersion(pipelineContext); - ExecuteCompletion(pipelineContext); - ExecuteDiagram(pipelineContext); - ExecuteErrorReporting(pipelineContext); + // TODO: Consider redesign where pipelineContext is not modifiable. + // + foreach (var subsystem in Subsystems) + { + if (subsystem is not null) + { + pipelineContext = subsystem.ExecuteIfNeeded(pipelineContext); + } + } } protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineContext pipelineContext) diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 6f3e70461e..8c2c36ce0d 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -76,7 +76,8 @@ protected internal void SetAnnotation(CliSymbol symbol, AnnotationId /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate /// A CliExit object with information such as whether the CLI should terminate - protected internal virtual CliExit Execute(PipelineContext pipelineContext) => CliExit.NotRun(pipelineContext.ParseResult); + protected internal virtual CliExit Execute(PipelineContext pipelineContext) + => CliExit.NotRun(pipelineContext.ParseResult); internal PipelineContext ExecuteIfNeeded(PipelineContext pipelineContext) => ExecuteIfNeeded(pipelineContext.ParseResult, pipelineContext); @@ -114,6 +115,7 @@ internal PipelineContext ExecuteIfNeeded(ParseResult? parseResult, PipelineConte /// The CLI configuration, which contains the RootCommand for customization /// True if parsing should continue // there might be a better design that supports a message // TODO: Because of this and similar usage, consider combining CLI declaration and config. ArgParse calls this the parser, which I like + // TODO: Why does Intitialize return a configuration? protected internal virtual CliConfiguration Initialize(InitializationContext context) => context.Configuration; diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index f55436a5dd..9bd3898174 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -11,8 +11,9 @@ public class ValueSubsystem : CliSubsystem { // @mhutch: Is the TryGet on the sparse dictionaries how we should handle a case where the annotations will be sparse to support lazy? If so, should we have another method on // the annotation wrapper, or an alternative struct when there a TryGet makes sense? This API needs review, maybe next Tuesday. - private PipelineContext? pipelineContext = null; + //private PipelineContext? pipelineContext = null; private Dictionary cachedValues = new(); + private ParseResult? parseResult = null; public ValueSubsystem(IAnnotationProvider? annotationProvider = null) : base(ValueAnnotations.Prefix, SubsystemKind.Version, annotationProvider) @@ -26,7 +27,7 @@ public ValueSubsystem(IAnnotationProvider? annotationProvider = null) // : ""; private bool TryGetDefaultValue(CliSymbol symbol, out T? defaultValue) { - if (TryGetAnnotation(symbol, ValueAnnotations.Value, out var objectValue)) + if (TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var objectValue)) { defaultValue = (T)objectValue; return true; @@ -47,15 +48,17 @@ public AnnotationAccessor?> DefaultValueCalculation => new(this, ValueAnnotations.DefaultValueCalculation); protected internal override bool GetIsActivated(ParseResult? parseResult) - => true; + { + this.parseResult = parseResult; + return true; + } protected internal override CliExit Execute(PipelineContext pipelineContext) { - this.pipelineContext = pipelineContext; - return CliExit.NotRun(pipelineContext.ParseResult); + parseResult ??= pipelineContext.ParseResult; + return base.Execute(pipelineContext); } - // TODO: Do it! Consider using a simple dictionary instead of the annotation (@mhutch) because with is not useful here private void SetValue(CliSymbol symbol, object? value) => cachedValues.Add(symbol, value); @@ -82,9 +85,9 @@ private bool TryGetValue(CliSymbol symbol, out T? value) { not null when TryGetValue(symbol, out var value) => value, // It has already been retrieved at least once - CliArgument argument when pipelineContext?.ParseResult?.GetValueResult(argument) is ValueResult valueResult // GetValue would always return a value + CliArgument argument when parseResult?.GetValueResult(argument) is { } valueResult // GetValue not used because it would always return a value => UseValue(symbol, valueResult.GetValue()), // Value was supplied during parsing, - CliOption option when pipelineContext?.ParseResult?.GetValueResult(option) is ValueResult valueResult // GetValue would always return a value + CliOption option when parseResult?.GetValueResult(option) is {} valueResult // GetValue not used because it would always return a value => UseValue(symbol, valueResult.GetValue()), // Value was supplied during parsing // Value was not supplied during parsing, determine default now not null when DefaultValueCalculation.TryGet(symbol, out var defaultValueCalculation) diff --git a/src/System.CommandLine/Parsing/ValueResult.cs b/src/System.CommandLine/Parsing/ValueResult.cs index 93d3d319dd..1759aa4a53 100644 --- a/src/System.CommandLine/Parsing/ValueResult.cs +++ b/src/System.CommandLine/Parsing/ValueResult.cs @@ -26,7 +26,9 @@ internal ValueResult( internal object? Value { get; } public T? GetValue() - => (T?)Value; + => Value is null + ? default + : (T?)Value; // This needs to be a collection because collection types have multiple tokens and they will not be simple offsets when response files are used // TODO: Consider more efficient ways to do this in the case where there is a single location From 262b6fc66603578510a709276f4998461fe9ef34 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Thu, 2 May 2024 15:53:12 -0400 Subject: [PATCH 080/150] Use generic annotation accessors Added Func generic annotation accesssor Moved value annotation accessor to own file Removed Try from ValueSubsystem to encourage ..Get/Set Cleanup, took some analyzer suggestions XML Docs improved --- .../Annotations/AnnotationAccessor.cs | 18 ----- .../Annotations/ValueAnnotationAccessor.cs | 63 +++++++++++++++++ .../Annotations/ValueAnnotations.cs | 14 ++-- .../ValueFuncAnnotationAccessor.cs | 63 +++++++++++++++++ .../Subsystems/CliSubsystem.cs | 46 +++++++++++-- .../ValueSubsystem.cs | 67 ++++++++----------- 6 files changed, 201 insertions(+), 70 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs index 5e8304c871..ec3b45e3c4 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs @@ -36,21 +36,3 @@ public struct AnnotationAccessor(CliSubsystem owner, AnnotationIdTrue if the value was found, false otherwise. public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, Id, out value); } - -/// -/// Allows associating an annotation with a . The annotation will be stored by the accessor's owner . -/// -public struct ValueAnnotationAccessor(CliSubsystem owner, AnnotationId id) -{ - /// - /// The ID of the annotation - /// - public AnnotationId Id { get; } - public readonly void Set(CliOption symbol, TSymbolValue value) - where TSymbolValue : TValue - => owner.SetAnnotation(symbol, id, value); - public readonly void Set(CliArgument symbol, TSymbolValue value) - where TSymbolValue : TValue - => owner.SetAnnotation(symbol, id, value); - public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, id, out value); -} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs new file mode 100644 index 0000000000..088787a64b --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs @@ -0,0 +1,63 @@ +// 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.CodeAnalysis; + +namespace System.CommandLine.Subsystems.Annotations; + +/// +/// Associates an annotation with a . The symbol must be an option or argument and the value must be of the same type as the symbol./>. +/// +/// +/// The annotation will be stored by the accessor's owner . +/// +/// The type of value to be stored +/// The subsystem that this annotation store data for. +/// The identifier for this annotation, since subsystems may have multiple annotations. +public struct ValueAnnotationAccessor(CliSubsystem owner, AnnotationId id) +{ + /// > + public AnnotationId Id { get; } + + /// > + public readonly void Set(CliOption symbol, TSymbolValue value) + where TSymbolValue : TValue + => owner.SetAnnotation(symbol, id, value); + + /// > + public readonly void Set(CliArgument symbol, TSymbolValue value) + where TSymbolValue : TValue + => owner.SetAnnotation(symbol, id, value); + + // TODO: Consider whether we need a version that takes a CliSymbol (ValueSymbol) + /// > + public readonly bool TryGet(CliOption symbol, [NotNullWhen(true)] out TValue? value) + where TSymbolValue : TValue + => TryGetInternal(symbol, out value); + + /// > + public readonly bool TryGet(CliArgument symbol, [NotNullWhen(true)] out TValue? value) + where TSymbolValue : TValue + => TryGetInternal(symbol, out value); + + /// > + /// + /// This overload will throw if the stored value cannot be converted to the type. + /// + /// + public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) + where TSymbolValue : TValue + => TryGetInternal(symbol, out value); + + private readonly bool TryGetInternal(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) + where TSymbolValue : TValue + { + if (owner.TryGetAnnotation(symbol, id, out var storedValue)) + { + value = (TSymbolValue)storedValue; + return true; + } + value = default; + return false; + } +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs index b867de30b2..8a2737cdf5 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs @@ -1,8 +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.CommandLine.Parsing; - namespace System.CommandLine.Subsystems.Annotations; /// @@ -10,9 +8,15 @@ namespace System.CommandLine.Subsystems.Annotations; /// public static class ValueAnnotations { - public static string Prefix { get; } = nameof(SubsystemKind.Value); + internal static string Prefix { get; } = nameof(SubsystemKind.Value); + /// + /// Provides Set and Get for default values + /// public static AnnotationId DefaultValue { get; } = new(Prefix, nameof(DefaultValue)); - public static AnnotationId?> DefaultValueCalculation { get; } = new(Prefix, nameof(DefaultValueCalculation)); - public static AnnotationId Value { get; } = new(Prefix, nameof(Value)); + + /// + /// Provides Set and Get for default value calculations + /// + public static AnnotationId> DefaultValueCalculation { get; } = new(Prefix, nameof(DefaultValueCalculation)); } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs new file mode 100644 index 0000000000..4184d4aafc --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs @@ -0,0 +1,63 @@ +// 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.CodeAnalysis; + +namespace System.CommandLine.Subsystems.Annotations; + +/// +/// Associates an annotation with a . The symbol must be an option or argument and the delegate must return a value of the same type as the symbol./>. +/// +/// +/// The annotation will be stored by the accessor's owner . +/// +/// The type of value to be stored +/// The subsystem that this annotation store data for. +/// The identifier for this annotation, since subsystems may have multiple annotations. +public struct ValueFuncAnnotationAccessor(CliSubsystem owner, AnnotationId> id) +{ + /// > + public AnnotationId> Id { get; } + + /// > + public readonly void Set(CliOption symbol, Func value) + where TSymbolValue : TValue + => owner.SetAnnotation(symbol, id, value); + + /// > + public readonly void Set(CliArgument symbol, Func value) + where TSymbolValue : TValue + => owner.SetAnnotation(symbol, id, value); + + // TODO: Consider whether we need a version that takes a CliSymbol (ValueSymbol) + /// > + public readonly bool TryGet(CliOption symbol, [NotNullWhen(true)] out Func? value) + where TSymbolValue : TValue + => TryGetInternal(symbol, out value); + + /// > + public readonly bool TryGet(CliArgument symbol, [NotNullWhen(true)] out Func? value) + where TSymbolValue : TValue + => TryGetInternal(symbol, out value); + + /// > + /// + /// This overload will throw if the stored value cannot be converted to the type. + /// + /// + public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out Func? value) + where TSymbolValue : TValue + => TryGetInternal(symbol, out value); + + private readonly bool TryGetInternal(CliSymbol symbol, [NotNullWhen(true)] out Func? value) + where TSymbolValue : TValue + { + if (owner.TryGetAnnotation(symbol, id, out Func? storedValue)) + { + value = storedValue; + return true; + } + value = default; + return false; + } +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 8c2c36ce0d..3ab8a87e47 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -22,10 +22,14 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv /// The name of the subsystem. /// public string Name { get; } + + /// + /// Defines the kind of subsystem, such as help or version + /// public SubsystemKind SubsystemKind { get; } - DefaultAnnotationProvider? _defaultProvider; - readonly IAnnotationProvider? _annotationProvider; + private DefaultAnnotationProvider? _defaultProvider; + private readonly IAnnotationProvider? _annotationProvider; /// /// Attempt to retrieve the value for the symbol and annotation ID from the annotation provider. @@ -53,6 +57,24 @@ protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId< return false; } + /// + /// A delegate that returns the value. + protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId> id, [NotNullWhen(true)] out Func? value) + { + if (_defaultProvider is not null && _defaultProvider.TryGet(symbol, id, out var storedValue)) + { + value = storedValue; + return true; + } + if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, id, out var storedValue2)) + { + value = storedValue2; + return true; + } + value = default; + return false; + } + /// /// Set the value for the symbol and annotation ID in the annotation provider. /// @@ -69,6 +91,16 @@ protected internal void SetAnnotation(CliSymbol symbol, AnnotationId + /// A delegate that returns the value. + protected internal void SetAnnotation(CliSymbol symbol, AnnotationId> id, Func value) + { + (_defaultProvider ??= new DefaultAnnotationProvider()).Set>(symbol, id, value); + } + + /// + /// The subystem executes, even if another subsystem has handled the operation. This is expected to be used in things like error reporting. + /// protected internal virtual bool RunsEvenIfAlreadyHandled { get; protected set; } /// @@ -76,17 +108,17 @@ protected internal void SetAnnotation(CliSymbol symbol, AnnotationId /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate /// A CliExit object with information such as whether the CLI should terminate - protected internal virtual CliExit Execute(PipelineContext pipelineContext) + protected internal virtual CliExit Execute(PipelineContext pipelineContext) => CliExit.NotRun(pipelineContext.ParseResult); internal PipelineContext ExecuteIfNeeded(PipelineContext pipelineContext) - => ExecuteIfNeeded(pipelineContext.ParseResult, pipelineContext); + => ExecuteIfNeeded(pipelineContext.ParseResult, pipelineContext); internal PipelineContext ExecuteIfNeeded(ParseResult? parseResult, PipelineContext pipelineContext) { - if( GetIsActivated(parseResult)) + if (GetIsActivated(parseResult)) { - Execute(pipelineContext ); + Execute(pipelineContext); } return pipelineContext; } @@ -120,7 +152,7 @@ protected internal virtual CliConfiguration Initialize(InitializationContext con => context.Configuration; // TODO: Determine if this is needed. - protected internal virtual CliExit TearDown(CliExit cliExit) + protected internal virtual CliExit TearDown(CliExit cliExit) => cliExit; } diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index 9bd3898174..d56ec5fcb0 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -1,65 +1,52 @@ // 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.Parsing; using System.CommandLine.Subsystems; using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine; -public class ValueSubsystem : CliSubsystem +public class ValueSubsystem(IAnnotationProvider? annotationProvider = null) + : CliSubsystem(ValueAnnotations.Prefix, SubsystemKind.Value, annotationProvider) { - // @mhutch: Is the TryGet on the sparse dictionaries how we should handle a case where the annotations will be sparse to support lazy? If so, should we have another method on - // the annotation wrapper, or an alternative struct when there a TryGet makes sense? This API needs review, maybe next Tuesday. - //private PipelineContext? pipelineContext = null; - private Dictionary cachedValues = new(); + private Dictionary cachedValues = []; private ParseResult? parseResult = null; - public ValueSubsystem(IAnnotationProvider? annotationProvider = null) - : base(ValueAnnotations.Prefix, SubsystemKind.Version, annotationProvider) - { } - - //internal void SetDefaultValue(CliSymbol symbol, object? defaultValue) - // => SetAnnotation(symbol, ValueAnnotations.DefaultValue, defaultValue); - //internal object? GetDefaultValue(CliSymbol symbol) - // => TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var defaultValue) - // ? defaultValue - // : ""; - private bool TryGetDefaultValue(CliSymbol symbol, out T? defaultValue) - { - if (TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var objectValue)) - { - defaultValue = (T)objectValue; - return true; - } - defaultValue = default; - return false; - } - public AnnotationAccessor DefaultValue + /// + /// Provides access to Get and Set methods for default values for symbols + /// + public ValueAnnotationAccessor DefaultValue => new(this, ValueAnnotations.DefaultValue); - internal void SetDefaultValueCalculation(CliSymbol symbol, Func factory) - => SetAnnotation(symbol, ValueAnnotations.DefaultValueCalculation, factory); - internal Func? GetDefaultValueCalculation(CliSymbol symbol) - => TryGetAnnotation?>(symbol, ValueAnnotations.DefaultValueCalculation, out var value) - ? value - : null; - public AnnotationAccessor?> DefaultValueCalculation - => new(this, ValueAnnotations.DefaultValueCalculation); + /// + /// Provides access to Get and Set methods for default value calculations for symbols + /// + public ValueFuncAnnotationAccessor DefaultValueCalculation + => new (this, ValueAnnotations.DefaultValueCalculation); + // It is possible that another subsystems GetIsActivated method will access a value. + // If this is called from a GetIsActivated method of a subsystem in the early termination group, + // it will fail. That is not an expected scenario. + /// + /// + /// Note to inheritors: Call base for all ValueSubsystem methods that you override to ensure correct behavior + /// protected internal override bool GetIsActivated(ParseResult? parseResult) { this.parseResult = parseResult; return true; } + /// + /// + /// Note to inheritors: Call base for all ValueSubsystem methods that you override to ensure correct behavior + /// protected internal override CliExit Execute(PipelineContext pipelineContext) { parseResult ??= pipelineContext.ParseResult; return base.Execute(pipelineContext); } - // TODO: Do it! Consider using a simple dictionary instead of the annotation (@mhutch) because with is not useful here private void SetValue(CliSymbol symbol, object? value) => cachedValues.Add(symbol, value); private bool TryGetValue(CliSymbol symbol, out T? value) @@ -78,7 +65,7 @@ private bool TryGetValue(CliSymbol symbol, out T? value) public T? GetValue(CliOption option) => GetValueInternal(option); public T? GetValue(CliArgument argument) - => GetValueInternal(argument); + => GetValueInternal(argument); private T? GetValueInternal(CliSymbol? symbol) => symbol switch @@ -90,10 +77,10 @@ not null when TryGetValue(symbol, out var value) CliOption option when parseResult?.GetValueResult(option) is {} valueResult // GetValue not used because it would always return a value => UseValue(symbol, valueResult.GetValue()), // Value was supplied during parsing // Value was not supplied during parsing, determine default now - not null when DefaultValueCalculation.TryGet(symbol, out var defaultValueCalculation) + not null when DefaultValueCalculation.TryGet(symbol, out var defaultValueCalculation) => UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)), - not null when TryGetDefaultValue(symbol, out var explicitValue) - => UseValue(symbol, explicitValue), + not null when DefaultValue.TryGet(symbol, out var explicitValue) + => UseValue(symbol, (T)explicitValue), //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) // => UseValue(symbol, GetEnvByName(envName)), null => throw new ArgumentNullException(nameof(symbol)), From f5473a93f9b727ce8611f97c2e9e092bfc2500a8 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Thu, 2 May 2024 17:36:30 -0400 Subject: [PATCH 081/150] Cleanup and fixes - CommandValueResult ctor internal - ValueResult ctor restricted to CliOption and CliArgument - Added XML comments - Removed dead code I was confident had been replaced --- src/System.CommandLine/ParseResult.cs | 51 +++------- .../Parsing/CommandValueResult.cs | 27 +++++- .../Parsing/SymbolLookupByName.cs | 31 ++++++- .../Parsing/SymbolResultTree.cs | 92 ------------------- src/System.CommandLine/Parsing/ValueResult.cs | 69 +++++++++++++- 5 files changed, 133 insertions(+), 137 deletions(-) diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 2ce4f4eca9..54d40c9303 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -82,44 +82,7 @@ internal ParseResult( Errors = errors is not null ? errors : Array.Empty(); } - //private Dictionary PopulateSymbolByName() - //{ - // var commands = GetSelfAndAncestors(CommandResult); - // var ret = new Dictionary { }; - - // foreach (var command in commands) - // { - // if (command.HasOptions) - // { - // foreach (var option in command.Options) - // { - // ret[option.Name] = option; - // } - // } - // if (command.HasArguments) - // { - // foreach (var argument in command.Arguments) - // { - // ret[argument.Name] = argument; - // } - // } - // } - // return ret; - - // static IEnumerable GetSelfAndAncestors(CommandResult commandResult) - // { - // var ret = new List { commandResult.Command }; - // while (commandResult.Parent is CommandResult parent) - // { - // commandResult = parent; - // ret.Add(parent.Command); - // } - // ret.Reverse(); - // return ret; - // } - //} - - public CliSymbol GetSymbolByName(string name, bool valuesOnly = false) + public CliSymbol? GetSymbolByName(string name, bool valuesOnly = false) { symbolLookupByName ??= new SymbolLookupByName(this); @@ -232,10 +195,22 @@ CommandLineText is null public override string ToString() => ParseDiagramAction.Diagram(this).ToString(); */ + /// + /// Gets the ValueResult, if any, for the specified option. + /// + /// The option for which to find a result. + /// A result for the specified option, or if it was not entered by the user. public ValueResult? GetValueResult(CliOption option) => GetValueResultInternal(option); + + /// + /// Gets the result, if any, for the specified argument. + /// + /// The argument for which to find a result. + /// A result for the specified argument, or if it was not entered by the user. public ValueResult? GetValueResult(CliArgument argument) => GetValueResultInternal(argument); + private ValueResult? GetValueResultInternal(CliSymbol symbol) => valueResultDictionary.TryGetValue(symbol, out var result) ? result diff --git a/src/System.CommandLine/Parsing/CommandValueResult.cs b/src/System.CommandLine/Parsing/CommandValueResult.cs index 4225fac089..a127da4e4c 100644 --- a/src/System.CommandLine/Parsing/CommandValueResult.cs +++ b/src/System.CommandLine/Parsing/CommandValueResult.cs @@ -5,15 +5,38 @@ namespace System.CommandLine.Parsing; +/// +/// Provides the publicly facing command result +/// +/// +/// The name is temporary as we expect to later name this CommandResult and the previous one to CommandResultInternal +/// public class CommandValueResult { - public CommandValueResult(CliCommand command, CommandValueResult parent) + /// + /// Creates a CommandValueResult instance + /// + /// The CliCommand that the result is for. + /// The parent command in the case of a CLI hierarchy, or null if there is no parent. + internal CommandValueResult(CliCommand command, CommandValueResult parent = null) { Command = command; Parent = parent; } + + /// + /// The ValueResult instances for user entered data. This is a sparse list. + /// public IEnumerable ValueResults { get; } = new List(); + + /// + /// The CliCommand that the result is for. + /// public CliCommand Command { get; } - public CommandValueResult Parent { get; } + + /// + /// The command's parent if one exists, otherwise, null + /// + public CommandValueResult? Parent { get; } } diff --git a/src/System.CommandLine/Parsing/SymbolLookupByName.cs b/src/System.CommandLine/Parsing/SymbolLookupByName.cs index 2e4b65e901..ccbacccc3c 100644 --- a/src/System.CommandLine/Parsing/SymbolLookupByName.cs +++ b/src/System.CommandLine/Parsing/SymbolLookupByName.cs @@ -12,9 +12,12 @@ namespace System.CommandLine.Parsing; // give correct results for many CLIs, and also, it built a dictionary for the full CLI tree, rather than just the // current commands and its ancestors, so in many cases, this will be faster. // -// Most importantly, that approach fails for options, like the previous global options, that appear on multiple +// Most importantly, the previous approach fails for options, like the previous global options, that appear on multiple // commands, since we are now explicitly putting them on all commands. +/// +/// Provides a mechanism to lookup symbols by their name. This searches the symbols corresponding to the current command and its ancestors. +/// public class SymbolLookupByName { private class CommandCache(CliCommand command) @@ -25,6 +28,11 @@ private class CommandCache(CliCommand command) private List cache; + /// + /// Creates a new symbol lookup tied to a specific parseResult. + /// + /// + // TODO: If needed, consider a static list/dictionary of ParseResult to make general use easier. public SymbolLookupByName(ParseResult parseResult) => cache = BuildCache(parseResult); @@ -121,12 +129,31 @@ private bool TryGetSymbolAndParentInternal(string name, return false; } + /// + /// Gets the symbol with the requested name that appears nearest to the starting command, which defaults to the current or leaf command. + /// + /// The name to search for + /// The command to start searching up from, which defaults to the current command. + /// If true, only the starting command and no ancestors are searched. + /// If true, commands are ignored and only options and arguments are found. + /// A tuple of the found symbol and its parent command. Throws if the name is not found. + /// Thrown if the name is not found. public (CliSymbol symbol, CliCommand parent) GetSymbolAndParent(string name, CliCommand? startCommand = null, bool skipAncestors = false, bool valuesOnly = false) => TryGetSymbolAndParentInternal(name, out var symbol, out var parent, out var errorMessage, startCommand, skipAncestors, valuesOnly) ? (symbol, parent) : throw new InvalidOperationException(errorMessage); - public bool TryGetSymbol(string name, out CliSymbol symbol, CliCommand? startCommand = null, bool skipAncestors = false, bool valuesOnly = false) + + /// + /// Returns true if the symbol is found, and provides the symbols as the `out` symbol parameter. + /// + /// The name to search for + /// An out parameter to receive the symbol if it is found. + /// The command to start searching up from, which defaults to the current command. + /// If true, only the starting command and no ancestors are searched. + /// If true, commands are ignored and only options and arguments are found. + /// True if a symbol with the requested name is found + public bool TryGetSymbol(string name, [NotNullWhen(true)] out CliSymbol? symbol, CliCommand? startCommand = null, bool skipAncestors = false, bool valuesOnly = false) => TryGetSymbolAndParentInternal(name, out symbol, out var _, out var _, startCommand, skipAncestors, valuesOnly); diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 74e25451cd..4b49b4f448 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -18,7 +18,6 @@ internal sealed class SymbolResultTree : Dictionary // TODO: Looks like this is a SymboNode/linked list because a symbol may appear multiple // places in the tree and multiple symbols will have the same short name. The question is // whether creating the multiple node instances is faster than just using lists. Could well be. - //private Dictionary? _symbolsByName; internal SymbolResultTree( CliCommand rootCommand, List? tokenizeErrors) @@ -47,11 +46,6 @@ internal SymbolResultTree( internal OptionResult? GetResult(CliOption option) => TryGetValue(option, out SymbolResult? result) ? (OptionResult)result : default; - //TODO: directives - /* - internal DirectiveResult? GetResult(CliDirective directive) - => TryGetValue(directive, out SymbolResult? result) ? (DirectiveResult)result : default; - */ // TODO: Determine how this is used. It appears to be O^n in the size of the tree and so if it is called multiple times, we should reconsider to avoid O^(N*M) internal IEnumerable GetChildren(SymbolResult parent) { @@ -109,91 +103,5 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com // } } - /* No longer used - public SymbolResult? GetResult(string name) - { - if (_symbolsByName is null) - { - _symbolsByName = new(); - // TODO: See if we can avoid populating the entire tree and just populate the portion/cone we need - PopulateSymbolsByName(_rootCommand); - } - - if (!_symbolsByName.TryGetValue(name, out SymbolNode? node)) - { - throw new ArgumentException($"No symbol result found with name \"{name}\"."); - } - - while (node is not null) - { - if (TryGetValue(node.Symbol, out var result)) - { - return result; - } - - node = node.Next; - } - - return null; - } - - // TODO: symbolsbyname - this is inefficient - // results for some values may not be queried at all, dependent on other options - // so we could avoid using their value factories and adding them to the dictionary - // could we sort by name allowing us to do a binary search instead of allocating a dictionary? - // could we add codepaths that query for specific kinds of symbols so they don't have to search all symbols? - // Additional Note: Couldn't commands know their children, and thus this involves querying the active command, and possibly the parents - private void PopulateSymbolsByName(CliCommand command) - { - if (command.HasArguments) - { - for (var i = 0; i < command.Arguments.Count; i++) - { - AddToSymbolsByName(command.Arguments[i]); - } - } - - if (command.HasOptions) - { - for (var i = 0; i < command.Options.Count; i++) - { - AddToSymbolsByName(command.Options[i]); - } - } - - if (command.HasSubcommands) - { - for (var i = 0; i < command.Subcommands.Count; i++) - { - var childCommand = command.Subcommands[i]; - AddToSymbolsByName(childCommand); - PopulateSymbolsByName(childCommand); - } - } - - // TODO: Explore removing closure here - void AddToSymbolsByName(CliSymbol symbol) - { - if (_symbolsByName!.TryGetValue(symbol.Name, out var node)) - { - if (symbol.Name == node.Symbol.Name && - symbol.FirstParent?.Symbol is { } parent && - parent == node.Symbol.FirstParent?.Symbol) - { - throw new InvalidOperationException($"Command {parent.Name} has more than one child named \"{symbol.Name}\"."); - } - - _symbolsByName[symbol.Name] = new(symbol) - { - Next = node - }; - } - else - { - _symbolsByName[symbol.Name] = new(symbol); - } - } - } - */ } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/ValueResult.cs b/src/System.CommandLine/Parsing/ValueResult.cs index 1759aa4a53..bc2bec25f2 100644 --- a/src/System.CommandLine/Parsing/ValueResult.cs +++ b/src/System.CommandLine/Parsing/ValueResult.cs @@ -5,9 +5,12 @@ namespace System.CommandLine.Parsing; +/// +/// The publicly facing class for argument and option data. +/// public class ValueResult { - internal ValueResult( + private ValueResult( CliSymbol valueSymbol, object? value, IEnumerable locations, @@ -22,22 +25,82 @@ internal ValueResult( Error = error; } + /// + /// Creates a new ValueResult instance + /// + /// The CliArgument the value is for. + /// The entered value. + /// The locations list. + /// True if parsing and converting the value was successful. + /// The CliError if parsing or converting failed, otherwise null. + internal ValueResult( + CliArgument argument, + object? value, + IEnumerable locations, + ValueResultOutcome outcome, + // TODO: Error should be an Enumerable and perhaps should not be here at all, only on ParseResult + string? error = null) + :this((CliSymbol)argument, value, locations, outcome,error) + { } + + /// + /// Creates a new ValueResult instance + /// + /// The CliOption the value is for. + /// The entered value. + /// The locations list. + /// True if parsing and converting the value was successful. + /// The CliError if parsing or converting failed, otherwise null. + internal ValueResult( + CliOption option, + object? value, + IEnumerable locations, + ValueResultOutcome outcome, + // TODO: Error should be an Enumerable and perhaps should not be here at all, only on ParseResult + string? error = null) + : this((CliSymbol)option, value, locations, outcome, error) + { } + + /// + /// The CliSymbol the value is for. This is always a CliOption or CliArgument. + /// public CliSymbol ValueSymbol { get; } + internal object? Value { get; } + /// + /// Returns the value, or the default for the type. + /// + /// The type to return + /// The value, cast to the requested type. public T? GetValue() => Value is null ? default : (T?)Value; - // This needs to be a collection because collection types have multiple tokens and they will not be simple offsets when response files are used - // TODO: Consider more efficient ways to do this in the case where there is a single location + /// + /// Gets the locations at which the tokens that made up the value appeared. + /// + /// + /// This needs to be a collection because collection types have multiple tokens and they will not be simple offsets when response files are used. + /// public IEnumerable Locations { get; } + /// + /// True when parsing and converting the value was successful + /// public ValueResultOutcome Outcome { get; } + /// + /// Parsing and conversion errors when parsing or converting failed. + /// public string? Error { get; } + /// + /// Returns test suitable for display. + /// + /// + /// public IEnumerable TextForDisplay() { throw new NotImplementedException(); From 92d4a8d7a6d5fd488c8a4e44f0928c6477d0185c Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Thu, 2 May 2024 19:28:12 -0400 Subject: [PATCH 082/150] Cleanup after reviewing PR and CI - Fixed a couple CI failures - Fixed up some whitespace --- .../Subsystems/CliSubsystem.cs | 2 +- src/System.CommandLine.Tests/ParserTests.cs | 10 +- src/System.CommandLine/ParseResult.cs | 297 +++++++++--------- .../Parsing/CommandValueResult.cs | 2 +- .../Parsing/OptionResult.cs | 8 +- .../Parsing/SymbolResultTree.cs | 21 +- 6 files changed, 170 insertions(+), 170 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 3ab8a87e47..94bd830110 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -29,7 +29,7 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv public SubsystemKind SubsystemKind { get; } private DefaultAnnotationProvider? _defaultProvider; - private readonly IAnnotationProvider? _annotationProvider; + private readonly IAnnotationProvider? _annotationProvider; /// /// Attempt to retrieve the value for the symbol and annotation ID from the annotation provider. diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 8ded430750..edf37ffed0 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -1256,12 +1256,12 @@ public void Single_option_arguments_that_match_option_aliases_are_parsed_correct } [Theory] - [InlineData("-x -y")] // - [InlineData("-x true -y")] // - [InlineData("-x:true -y")] // - [InlineData("-x=true -y")] // + [InlineData("-x -y")] + [InlineData("-x true -y")] + [InlineData("-x:true -y")] + [InlineData("-x=true -y")] [InlineData("-x -y true")] - [InlineData("-x true -y true")] // + [InlineData("-x true -y true")] [InlineData("-x:true -y:true")] [InlineData("-x=true -y:true")] public void Boolean_options_are_not_greedy(string commandLine) diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 54d40c9303..0a8b300b1c 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -15,16 +15,15 @@ namespace System.CommandLine public sealed class ParseResult { private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); - private Dictionary symbolByName = null; - private SymbolLookupByName symbolLookupByName = null; + private SymbolLookupByName? symbolLookupByName = null; - private readonly CommandResult _rootCommandResult; + private readonly CommandResult? _rootCommandResult; // TODO: unmatched tokens, invocation, completion /* - private readonly IReadOnlyList _unmatchedTokens; - private CompletionContext? _completionContext; - private readonly CliAction? _action; - private readonly List? _preActions; + private readonly IReadOnlyList _unmatchedTokens; + private CompletionContext? _completionContext; + private readonly CliAction? _action; + private readonly List? _preActions; */ internal ParseResult( @@ -37,14 +36,14 @@ internal ParseResult( List tokens, */ // TODO: unmatched tokens - // List? unmatchedTokens, + // List? unmatchedTokens, List? errors, // TODO: commandLineText should be string array string? commandLineText = null //, - // TODO: invocation + // TODO: invocation /* - CliAction? action = null, - List? preActions = null) + CliAction? action = null, + List? preActions = null) */ ) { @@ -54,8 +53,8 @@ internal ParseResult( valueResultDictionary = symbolResultTree.BuildValueResultDictionary(); // TODO: invocation /* - _action = action; - _preActions = preActions; + _action = action; + _preActions = preActions; */ /* // skip the root command when populating Tokens property @@ -77,7 +76,7 @@ internal ParseResult( CommandLineText = commandLineText; // TODO: unmatched tokens - // _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; + // _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; Errors = errors is not null ? errors : Array.Empty(); } @@ -93,7 +92,7 @@ internal ParseResult( // TODO: check that constructing empty ParseResult directly is correct /* - internal static ParseResult Empty() => new CliRootCommand().Parse(Array.Empty()); + internal static ParseResult Empty() => new CliRootCommand().Parse(Array.Empty()); */ /// @@ -244,12 +243,12 @@ CommandLineText is null // TODO: Directives /* - /// - /// Gets the result, if any, for the specified directive. - /// - /// The directive for which to find a result. - /// A result for the specified directive, or if it was not provided. - public DirectiveResult? GetResult(CliDirective directive) => _rootCommandResult.GetResult(directive); + /// + /// Gets the result, if any, for the specified directive. + /// + /// The directive for which to find a result. + /// A result for the specified directive, or if it was not provided. + public DirectiveResult? GetResult(CliDirective directive) => _rootCommandResult.GetResult(directive); */ /* Replaced with GetValueResult /// @@ -262,167 +261,167 @@ CommandLineText is null */ // TODO: completion, invocation /* - /// - /// Gets completions based on a given parse result. - /// - /// The position at which completions are requested. - /// A set of completions for completion. - public IEnumerable GetCompletions( - int? position = null) - { - SymbolResult currentSymbolResult = SymbolToComplete(position); + /// + /// Gets completions based on a given parse result. + /// + /// The position at which completions are requested. + /// A set of completions for completion. + public IEnumerable GetCompletions( + int? position = null) + { + SymbolResult currentSymbolResult = SymbolToComplete(position); - CliSymbol currentSymbol = currentSymbolResult switch - { - ArgumentResult argumentResult => argumentResult.Argument, - OptionResult optionResult => optionResult.Option, - DirectiveResult directiveResult => directiveResult.Directive, - _ => ((CommandResult)currentSymbolResult).Command - }; + CliSymbol currentSymbol = currentSymbolResult switch + { + ArgumentResult argumentResult => argumentResult.Argument, + OptionResult optionResult => optionResult.Option, + DirectiveResult directiveResult => directiveResult.Directive, + _ => ((CommandResult)currentSymbolResult).Command + }; - var context = GetCompletionContext(); + var context = GetCompletionContext(); - if (position is not null && - context is TextCompletionContext tcc) - { - context = tcc.AtCursorPosition(position.Value); - } + if (position is not null && + context is TextCompletionContext tcc) + { + context = tcc.AtCursorPosition(position.Value); + } - var completions = currentSymbol.GetCompletions(context); + var completions = currentSymbol.GetCompletions(context); - string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult - ? OptionsWithArgumentLimitReached(commandResult) - : Array.Empty(); + string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult + ? OptionsWithArgumentLimitReached(commandResult) + : Array.Empty(); - completions = - completions.Where(item => optionsWithArgumentLimitReached.All(s => s != item.Label)); + completions = + completions.Where(item => optionsWithArgumentLimitReached.All(s => s != item.Label)); - return completions; + return completions; - static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => - commandResult - .Children - .OfType() - .Where(c => c.IsArgumentLimitReached) - .Select(o => o.Option) - .SelectMany(c => new[] { c.Name }.Concat(c.Aliases)) - .ToArray(); - } + static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => + commandResult + .Children + .OfType() + .Where(c => c.IsArgumentLimitReached) + .Select(o => o.Option) + .SelectMany(c => new[] { c.Name }.Concat(c.Aliases)) + .ToArray(); + } - /// - /// Invokes the appropriate command handler for a parsed command line input. - /// - /// A token that can be used to cancel an invocation. - /// A task whose result can be used as a process exit code. - public Task InvokeAsync(CancellationToken cancellationToken = default) - => InvocationPipeline.InvokeAsync(this, cancellationToken); - - /// - /// Invokes the appropriate command handler for a parsed command line input. - /// - /// A value that can be used as a process exit code. - public int Invoke() - { - var useAsync = false; + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// A token that can be used to cancel an invocation. + /// A task whose result can be used as a process exit code. + public Task InvokeAsync(CancellationToken cancellationToken = default) + => InvocationPipeline.InvokeAsync(this, cancellationToken); - if (Action is AsynchronousCliAction) - { - useAsync = true; - } - else if (PreActions is not null) - { - for (var i = 0; i < PreActions.Count; i++) - { - var action = PreActions[i]; - if (action is AsynchronousCliAction) - { - useAsync = true; - break; - } - } - } + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// A value that can be used as a process exit code. + public int Invoke() + { + var useAsync = false; - if (useAsync) - { - return InvocationPipeline.InvokeAsync(this, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); - } - else + if (Action is AsynchronousCliAction) + { + useAsync = true; + } + else if (PreActions is not null) + { + for (var i = 0; i < PreActions.Count; i++) + { + var action = PreActions[i]; + if (action is AsynchronousCliAction) { - return InvocationPipeline.Invoke(this); + useAsync = true; + break; } } + } - /// - /// Gets the for parsed result. The handler represents the action - /// that will be performed when the parse result is invoked. - /// - public CliAction? Action => _action ?? CommandResult.Command.Action; + if (useAsync) + { + return InvocationPipeline.InvokeAsync(this, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + } + else + { + return InvocationPipeline.Invoke(this); + } + } - internal IReadOnlyList? PreActions => _preActions; + /// + /// Gets the for parsed result. The handler represents the action + /// that will be performed when the parse result is invoked. + /// + public CliAction? Action => _action ?? CommandResult.Command.Action; - private SymbolResult SymbolToComplete(int? position = null) - { - var commandResult = CommandResult; + internal IReadOnlyList? PreActions => _preActions; - var allSymbolResultsForCompletion = AllSymbolResultsForCompletion(); + private SymbolResult SymbolToComplete(int? position = null) + { + var commandResult = CommandResult; + + var allSymbolResultsForCompletion = AllSymbolResultsForCompletion(); - var currentSymbol = allSymbolResultsForCompletion.Last(); + var currentSymbol = allSymbolResultsForCompletion.Last(); - return currentSymbol; + return currentSymbol; - IEnumerable AllSymbolResultsForCompletion() + IEnumerable AllSymbolResultsForCompletion() + { + foreach (var item in commandResult.AllSymbolResults()) + { + if (item is CommandResult command) { - foreach (var item in commandResult.AllSymbolResults()) - { - if (item is CommandResult command) - { - yield return command; - } - else if (item is OptionResult option) - { - if (WillAcceptAnArgument(this, position, option)) - { - yield return option; - } - } - } + yield return command; } - - static bool WillAcceptAnArgument( - ParseResult parseResult, - int? position, - OptionResult optionResult) + else if (item is OptionResult option) { - if (optionResult.Implicit) + if (WillAcceptAnArgument(this, position, option)) { - return false; + yield return option; } + } + } + } - if (!optionResult.IsArgumentLimitReached) - { - return true; - } + static bool WillAcceptAnArgument( + ParseResult parseResult, + int? position, + OptionResult optionResult) + { + if (optionResult.Implicit) + { + return false; + } - var completionContext = parseResult.GetCompletionContext(); + if (!optionResult.IsArgumentLimitReached) + { + return true; + } - if (completionContext is TextCompletionContext textCompletionContext) - { - if (position.HasValue) - { - textCompletionContext = textCompletionContext.AtCursorPosition(position.Value); - } + var completionContext = parseResult.GetCompletionContext(); - if (textCompletionContext.WordToComplete.Length > 0) - { - var tokenToComplete = parseResult.Tokens.Last(t => t.Value == textCompletionContext.WordToComplete); + if (completionContext is TextCompletionContext textCompletionContext) + { + if (position.HasValue) + { + textCompletionContext = textCompletionContext.AtCursorPosition(position.Value); + } - return optionResult.Tokens.Contains(tokenToComplete); - } - } + if (textCompletionContext.WordToComplete.Length > 0) + { + var tokenToComplete = parseResult.Tokens.Last(t => t.Value == textCompletionContext.WordToComplete); - return !optionResult.IsArgumentLimitReached; + return optionResult.Tokens.Contains(tokenToComplete); } } - */ + + return !optionResult.IsArgumentLimitReached; + } + } + */ } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/CommandValueResult.cs b/src/System.CommandLine/Parsing/CommandValueResult.cs index a127da4e4c..60f403b5c1 100644 --- a/src/System.CommandLine/Parsing/CommandValueResult.cs +++ b/src/System.CommandLine/Parsing/CommandValueResult.cs @@ -18,7 +18,7 @@ public class CommandValueResult /// /// The CliCommand that the result is for. /// The parent command in the case of a CLI hierarchy, or null if there is no parent. - internal CommandValueResult(CliCommand command, CommandValueResult parent = null) + internal CommandValueResult(CliCommand command, CommandValueResult? parent = null) { Command = command; Parent = parent; diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/OptionResult.cs index 794cf1d0b9..5917833625 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/OptionResult.cs @@ -70,10 +70,10 @@ public ValueResult ValueResult // TODO: do we even need IdentifierTokenCount /* - /// - /// The number of occurrences of an identifier token matching the option. - /// - public int IdentifierTokenCount { get; internal set; } + /// + /// The number of occurrences of an identifier token matching the option. + /// + public int IdentifierTokenCount { get; internal set; } */ /// public override string ToString() => $"{nameof(OptionResult)}: {IdentifierToken?.Value ?? Option.Name} {string.Join(" ", Tokens.Select(t => t.Value))}"; diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 4b49b4f448..eb281aad3a 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -12,7 +12,7 @@ internal sealed class SymbolResultTree : Dictionary internal List? Errors; // TODO: unmatched tokens /* - internal List? UnmatchedTokens; + internal List? UnmatchedTokens; */ // TODO: Looks like this is a SymboNode/linked list because a symbol may appear multiple @@ -89,19 +89,20 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com { /* // TODO: unmatched tokens - (UnmatchedTokens ??= new()).Add(token); + (UnmatchedTokens ??= new()).Add(token); - if (commandResult.Command.TreatUnmatchedTokensAsErrors) - { - if (commandResult != rootCommandResult && !rootCommandResult.Command.TreatUnmatchedTokensAsErrors) - { - return; - } + if (commandResult.Command.TreatUnmatchedTokensAsErrors) + { + if (commandResult != rootCommandResult && !rootCommandResult.Command.TreatUnmatchedTokensAsErrors) + { + return; + } */ AddError(new ParseError(LocalizationResources.UnrecognizedCommandOrArgument(token.Value), commandResult)); - // } + /* + } + */ } - } } \ No newline at end of file From d8b18edcab21ec26e74777947f1d33a21e208076 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Fri, 3 May 2024 11:42:29 -0400 Subject: [PATCH 083/150] Response to PR review --- .../ValueSubsystemTests.cs | 30 +++++++++---------- .../ParseResultValueTests.cs | 9 ++++-- src/System.CommandLine.Tests/ParserTests.cs | 2 +- src/System.CommandLine/ParseResult.cs | 2 +- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index b7b64d869f..459940d80d 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -34,41 +34,41 @@ public void Value_is_always_activated() public void ValueSubsystem_returns_values_that_are_entered() { var consoleHack = new ConsoleHack().RedirectToBuffer(true); - CliOption option1 = new CliOption("--intValue"); + CliOption option = new CliOption("--intValue"); CliRootCommand rootCommand = [ new CliCommand("x") { - option1 + option }]; var configuration = new CliConfiguration(rootCommand); var pipeline = Pipeline.CreateEmpty(); pipeline.Value = new ValueSubsystem(); - const int expected1 = 42; - var input = $"x --intValue {expected1}"; + const int expected = 42; + var input = $"x --intValue {expected}"; var parseResult = pipeline.Parse(configuration, input); // assigned for debugging pipeline.Execute(configuration, input, consoleHack); - pipeline.Value.GetValue(option1).Should().Be(expected1); + pipeline.Value.GetValue(option).Should().Be(expected); } [Fact] public void ValueSubsystem_returns_default_value_when_no_value_is_entered() { var consoleHack = new ConsoleHack().RedirectToBuffer(true); - CliOption option1 = new CliOption("--intValue"); - CliRootCommand rootCommand = [option1]; + CliOption option = new CliOption("--intValue"); + CliRootCommand rootCommand = [option]; var configuration = new CliConfiguration(rootCommand); var pipeline = Pipeline.CreateEmpty(); pipeline.Value = new ValueSubsystem(); - pipeline.Value.DefaultValue.Set(option1, 43); - const int expected1 = 43; + pipeline.Value.DefaultValue.Set(option, 43); + const int expected = 43; var input = $""; var parseResult = pipeline.Parse(configuration, input); // assigned for debugging pipeline.Execute(configuration, input, consoleHack); - pipeline.Value.GetValue(option1).Should().Be(expected1); + pipeline.Value.GetValue(option).Should().Be(expected); } @@ -76,19 +76,19 @@ public void ValueSubsystem_returns_default_value_when_no_value_is_entered() public void ValueSubsystem_returns_calculated_default_value_when_no_value_is_entered() { var consoleHack = new ConsoleHack().RedirectToBuffer(true); - CliOption option1 = new CliOption("--intValue"); - CliRootCommand rootCommand = [option1]; + CliOption option = new CliOption("--intValue"); + CliRootCommand rootCommand = [option]; var configuration = new CliConfiguration(rootCommand); var pipeline = Pipeline.CreateEmpty(); pipeline.Value = new ValueSubsystem(); var x = 42; - pipeline.Value.DefaultValueCalculation.Set(option1, () => x + 2); - const int expected1 = 44; + pipeline.Value.DefaultValueCalculation.Set(option, () => x + 2); + const int expected = 44; var input = $""; var parseResult = pipeline.Parse(configuration, input); // assigned for debugging pipeline.Execute(configuration, input, consoleHack); - pipeline.Value.GetValue(option1).Should().Be(expected1); + pipeline.Value.GetValue(option).Should().Be(expected); } } diff --git a/src/System.CommandLine.Tests/ParseResultValueTests.cs b/src/System.CommandLine.Tests/ParseResultValueTests.cs index 63b51015f6..18ce4881e2 100644 --- a/src/System.CommandLine.Tests/ParseResultValueTests.cs +++ b/src/System.CommandLine.Tests/ParseResultValueTests.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Parsing; -using System.Linq; using FluentAssertions; +using FluentAssertions.Execution; using Xunit; namespace System.CommandLine.Tests; @@ -26,8 +26,11 @@ public void Symbol_found_by_name() var symbol1 = parseResult.GetSymbolByName("--opt1"); var symbol2 = parseResult.GetSymbolByName("--opt2"); - symbol1.Should().Be(option1); - symbol2.Should().Be(option2); + using (new AssertionScope()) + { + symbol1.Should().Be(option1); + symbol2.Should().Be(option2); + } } [Fact] diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index edf37ffed0..1c67218800 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -1883,7 +1883,7 @@ public void Locations_correct_for_collection() { option1 }; - var expectedOuterLocation = new Location("testhost", Location.User, -1, null); + var expectedOuterLocation = new Location(CliExecutable.ExecutableName, Location.User, -1, null); var expectedLocation1 = new Location("Kirk", Location.User, 2, expectedOuterLocation); var expectedLocation2 = new Location("Spock", Location.User, 3, expectedOuterLocation); var expectedLocation3 = new Location("Uhura", Location.User, 4, expectedOuterLocation); diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 0a8b300b1c..303617c171 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -17,7 +17,7 @@ public sealed class ParseResult private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); private SymbolLookupByName? symbolLookupByName = null; - private readonly CommandResult? _rootCommandResult; + private readonly CommandResult _rootCommandResult; // TODO: unmatched tokens, invocation, completion /* private readonly IReadOnlyList _unmatchedTokens; From 6094c81813c85c6147c87278bebdd4755a278b3d Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Thu, 9 May 2024 09:41:14 -0400 Subject: [PATCH 084/150] Respond to group PR Review --- .../ValueSubsystemTests.cs | 5 +- src/System.CommandLine.Subsystems/Pipeline.cs | 7 +-- .../Annotations/AnnotationAccessor.cs | 6 -- .../Annotations/ValueAnnotationAccessor.cs | 8 ++- .../ValueFuncAnnotationAccessor.cs | 27 ++++----- .../Subsystems/CliSubsystem.cs | 2 +- .../ValueSubsystem.cs | 40 ++++++++------ .../ParseResultValueTests.cs | 37 +++++++------ src/System.CommandLine.Tests/ParserTests.cs | 6 +- src/System.CommandLine/ParseResult.cs | 4 +- .../Parsing/OptionResult.cs | 6 +- .../Parsing/SymbolLookupByName.cs | 55 ++++++++++++------- .../Parsing/SymbolResultTree.cs | 2 +- src/System.CommandLine/Parsing/ValueResult.cs | 25 +++++++-- 14 files changed, 127 insertions(+), 103 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index 459940d80d..4b724996c9 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -11,7 +11,7 @@ namespace System.CommandLine.Subsystems.Tests; public class ValueSubsystemTests { [Fact] - public void Value_is_always_activated() + public void ValueSubsystem_is_activated_by_default() { CliRootCommand rootCommand = [ new CliCommand("x") @@ -65,7 +65,6 @@ public void ValueSubsystem_returns_default_value_when_no_value_is_entered() const int expected = 43; var input = $""; - var parseResult = pipeline.Parse(configuration, input); // assigned for debugging pipeline.Execute(configuration, input, consoleHack); pipeline.Value.GetValue(option).Should().Be(expected); @@ -84,7 +83,7 @@ public void ValueSubsystem_returns_calculated_default_value_when_no_value_is_ent var x = 42; pipeline.Value.DefaultValueCalculation.Set(option, () => x + 2); const int expected = 44; - var input = $""; + var input = ""; var parseResult = pipeline.Parse(configuration, input); // assigned for debugging pipeline.Execute(configuration, input, consoleHack); diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index d2be0b2dfd..93a1ff3c74 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -4,7 +4,6 @@ using System.CommandLine.Directives; using System.CommandLine.Parsing; using System.CommandLine.Subsystems; -using System.Reflection.PortableExecutable; namespace System.CommandLine; @@ -30,7 +29,7 @@ public static Pipeline Create(HelpSubsystem? help = null, Value = value ?? new ValueSubsystem() }; - public static Pipeline CreateEmpty() + public static Pipeline CreateEmpty() => new(); private Pipeline() { } @@ -83,9 +82,9 @@ protected virtual void InitializeSubsystems(InitializationContext context) { foreach (var subsystem in Subsystems) { - if ( subsystem is not null) + if (subsystem is not null) { - subsystem.Initialize(context); + subsystem.Initialize(context); } } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs index ec3b45e3c4..a6a0abb906 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs @@ -8,12 +8,6 @@ namespace System.CommandLine.Subsystems.Annotations; /// /// Allows associating an annotation with a . The annotation will be stored by the accessor's owner . /// -/// -/// The annotation will be stored by the accessor's owner . -/// -/// The type of value to be stored -/// The subsystem that this annotation store data for. -/// The identifier for this annotation, since subsystems may have multiple annotations. public struct AnnotationAccessor(CliSubsystem owner, AnnotationId id) { /// diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs index 088787a64b..613ce48c7f 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs @@ -54,8 +54,12 @@ private readonly bool TryGetInternal(CliSymbol symbol, [NotNullWhe { if (owner.TryGetAnnotation(symbol, id, out var storedValue)) { - value = (TSymbolValue)storedValue; - return true; + if (storedValue is TSymbolValue symbolValue) + { + value = symbolValue; + return true; + } + throw new ArgumentException("The requested type is incorrect.", nameof(symbol)); } value = default; return false; diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs index 4184d4aafc..e30ec57885 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs @@ -6,7 +6,7 @@ namespace System.CommandLine.Subsystems.Annotations; /// -/// Associates an annotation with a . The symbol must be an option or argument and the delegate must return a value of the same type as the symbol./>. +/// Associates an annotation with a . The symbol must be an option or argument and the delegate must return a value of the same type as the symbol. /// /// /// The annotation will be stored by the accessor's owner . @@ -14,43 +14,38 @@ namespace System.CommandLine.Subsystems.Annotations; /// The type of value to be stored /// The subsystem that this annotation store data for. /// The identifier for this annotation, since subsystems may have multiple annotations. +// TODO: If we keep this approach, consider making this class more general purpose or replacing use with AnnotationAccessior public struct ValueFuncAnnotationAccessor(CliSubsystem owner, AnnotationId> id) { /// > public AnnotationId> Id { get; } /// > - public readonly void Set(CliOption symbol, Func value) - where TSymbolValue : TValue + public readonly void Set(CliOption symbol, Func value) => owner.SetAnnotation(symbol, id, value); /// > - public readonly void Set(CliArgument symbol, Func value) - where TSymbolValue : TValue + public readonly void Set(CliArgument symbol, Func value) => owner.SetAnnotation(symbol, id, value); // TODO: Consider whether we need a version that takes a CliSymbol (ValueSymbol) /// > - public readonly bool TryGet(CliOption symbol, [NotNullWhen(true)] out Func? value) - where TSymbolValue : TValue - => TryGetInternal(symbol, out value); + public readonly bool TryGet(CliOption symbol, [NotNullWhen(true)] out Func? value) + => TryGetInternal(symbol, out value); /// > - public readonly bool TryGet(CliArgument symbol, [NotNullWhen(true)] out Func? value) - where TSymbolValue : TValue - => TryGetInternal(symbol, out value); + public readonly bool TryGet(CliArgument symbol, [NotNullWhen(true)] out Func? value) + => TryGetInternal(symbol, out value); /// > /// /// This overload will throw if the stored value cannot be converted to the type. /// /// - public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out Func? value) - where TSymbolValue : TValue - => TryGetInternal(symbol, out value); + public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out Func? value) + => TryGetInternal(symbol, out value); - private readonly bool TryGetInternal(CliSymbol symbol, [NotNullWhen(true)] out Func? value) - where TSymbolValue : TValue + private readonly bool TryGetInternal(CliSymbol symbol, [NotNullWhen(true)] out Func? value) { if (owner.TryGetAnnotation(symbol, id, out Func? storedValue)) { diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 94bd830110..25ac7bd213 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -29,7 +29,7 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv public SubsystemKind SubsystemKind { get; } private DefaultAnnotationProvider? _defaultProvider; - private readonly IAnnotationProvider? _annotationProvider; + private readonly IAnnotationProvider? _annotationProvider; /// /// Attempt to retrieve the value for the symbol and annotation ID from the annotation provider. diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index d56ec5fcb0..bf3f650d94 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -6,7 +6,7 @@ namespace System.CommandLine; -public class ValueSubsystem(IAnnotationProvider? annotationProvider = null) +public class ValueSubsystem(IAnnotationProvider? annotationProvider = null) : CliSubsystem(ValueAnnotations.Prefix, SubsystemKind.Value, annotationProvider) { private Dictionary cachedValues = []; @@ -22,7 +22,7 @@ public ValueAnnotationAccessor DefaultValue /// Provides access to Get and Set methods for default value calculations for symbols /// public ValueFuncAnnotationAccessor DefaultValueCalculation - => new (this, ValueAnnotations.DefaultValueCalculation); + => new(this, ValueAnnotations.DefaultValueCalculation); // It is possible that another subsystems GetIsActivated method will access a value. // If this is called from a GetIsActivated method of a subsystem in the early termination group, @@ -48,14 +48,17 @@ protected internal override CliExit Execute(PipelineContext pipelineContext) } private void SetValue(CliSymbol symbol, object? value) - => cachedValues.Add(symbol, value); + { + cachedValues[symbol] = value; + } + private bool TryGetValue(CliSymbol symbol, out T? value) { if (cachedValues.TryGetValue(symbol, out var objectValue)) { value = objectValue is null ? default - :(T)objectValue; + : (T)objectValue; return true; } value = default; @@ -68,25 +71,34 @@ private bool TryGetValue(CliSymbol symbol, out T? value) => GetValueInternal(argument); private T? GetValueInternal(CliSymbol? symbol) - => symbol switch + { + return symbol switch { not null when TryGetValue(symbol, out var value) => value, // It has already been retrieved at least once - CliArgument argument when parseResult?.GetValueResult(argument) is { } valueResult // GetValue not used because it would always return a value + CliArgument argument when parseResult?.GetValueResult(argument) is { } valueResult // GetValue not used because it would always return a value => UseValue(symbol, valueResult.GetValue()), // Value was supplied during parsing, - CliOption option when parseResult?.GetValueResult(option) is {} valueResult // GetValue not used because it would always return a value + CliOption option when parseResult?.GetValueResult(option) is { } valueResult // GetValue not used because it would always return a value => UseValue(symbol, valueResult.GetValue()), // Value was supplied during parsing // Value was not supplied during parsing, determine default now - not null when DefaultValueCalculation.TryGet(symbol, out var defaultValueCalculation) - => UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)), - not null when DefaultValue.TryGet(symbol, out var explicitValue) - => UseValue(symbol, (T)explicitValue), + // configuration values go here in precedence //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) // => UseValue(symbol, GetEnvByName(envName)), + not null when DefaultValueCalculation.TryGet(symbol, out var defaultValueCalculation) + => UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)), + not null when DefaultValue.TryGet(symbol, out var explicitValue) + => UseValue(symbol, (T)explicitValue), null => throw new ArgumentNullException(nameof(symbol)), _ => UseValue(symbol, default(T)) }; + TValue? UseValue(CliSymbol symbol, TValue? value) + { + SetValue(symbol, value); + return value; + } + } + private static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) { var objectValue = defaultValueCalculation(); @@ -95,10 +107,4 @@ not null when DefaultValue.TryGet(symbol, out var explicitValue) : (T)objectValue; return value; } - - private T? UseValue(CliSymbol symbol, T? value) - { - SetValue(symbol, value); - return value; - } } diff --git a/src/System.CommandLine.Tests/ParseResultValueTests.cs b/src/System.CommandLine.Tests/ParseResultValueTests.cs index 18ce4881e2..b3d7f986e2 100644 --- a/src/System.CommandLine.Tests/ParseResultValueTests.cs +++ b/src/System.CommandLine.Tests/ParseResultValueTests.cs @@ -17,10 +17,10 @@ public void Symbol_found_by_name() var option2 = new CliOption("--opt2"); var rootCommand = new CliRootCommand - { - option1, - option2 - }; + { + option1, + option2 + }; var parseResult = CliParser.Parse(rootCommand, "--opt1 Kirk"); @@ -28,31 +28,32 @@ public void Symbol_found_by_name() var symbol2 = parseResult.GetSymbolByName("--opt2"); using (new AssertionScope()) { - symbol1.Should().Be(option1); - symbol2.Should().Be(option2); + symbol1.Should().Be(option1, "because option1 should be found for --opt1" ); + symbol2.Should().Be(option2, "because option2 should be found for --opt2"); } } [Fact] public void Nearest_symbol_found_when_multiple() { - var option1 = new CliOption("--opt1", "-1"); - var option2 = new CliOption("--opt1", "-2"); + // both options have the same name as that is the point of the test + var optionA = new CliOption("--opt1", "-1"); + var optionB = new CliOption("--opt1", "-2"); var command = new CliCommand("subcommand") - { - option2 - }; + { + optionB + }; var rootCommand = new CliRootCommand - { - command, - option1 - }; + { + command, + optionA + }; - var parseResult = CliParser.Parse(rootCommand, "subcommand --opt2 Spock"); + var parseResult = CliParser.Parse(rootCommand, "subcommand"); var symbol = parseResult.GetSymbolByName("--opt1"); - symbol.Should().Be(option2); + symbol.Should().Be(optionB, "because it is closer to the leaf/executing command"); } -} \ No newline at end of file +} diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 1c67218800..899552c804 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -1890,10 +1890,8 @@ public void Locations_correct_for_collection() var parseResult = CliParser.Parse(rootCommand, "subcommand --opt1 Kirk Spock Uhura"); - var commandResult = parseResult.CommandResult; - parseResult.GetValue(option1); - var result1 = parseResult.GetValueResult(option1); - result1.Locations.Should().BeEquivalentTo([expectedLocation1, expectedLocation2, expectedLocation3]); + var result = parseResult.GetValueResult(option1); + result.Locations.Should().BeEquivalentTo([expectedLocation1, expectedLocation2, expectedLocation3]); } [Fact] diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 303617c171..6a15a0ba61 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -87,7 +87,7 @@ internal ParseResult( symbolLookupByName ??= new SymbolLookupByName(this); return symbolLookupByName.TryGetSymbol(name, out var symbol, valuesOnly: valuesOnly) ? symbol - : throw new ArgumentException($"No symbol result found with name \"{name}\"."); + : throw new ArgumentException($"No symbol result found with name \"{name}\".", nameof(name)); } // TODO: check that constructing empty ParseResult directly is correct @@ -206,7 +206,7 @@ CommandLineText is null /// Gets the result, if any, for the specified argument. /// /// The argument for which to find a result. - /// A result for the specified argument, or if it was not entered by the user. + /// A result for the specified argument, or if it was not entered by the user. public ValueResult? GetValueResult(CliArgument argument) => GetValueResultInternal(argument); diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/OptionResult.cs index 5917833625..9bfa62babb 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/OptionResult.cs @@ -36,10 +36,10 @@ public ValueResult ValueResult // TODO: Make sure errors are added var conversionResult = ArgumentConversionResult .ConvertIfNeeded(Option.Argument.ValueType); - var conversionValue = conversionResult.Result switch + object? conversionValue = conversionResult.Result switch { - ArgumentConversionResultType.Successful => conversionResult.Value!, - ArgumentConversionResultType.NoArgument => default!, + ArgumentConversionResultType.Successful => conversionResult.Value, + ArgumentConversionResultType.NoArgument => default, _ => default // This is an error condition, and is handled below }; var locations = Tokens.Select(token => token.Location).ToArray(); diff --git a/src/System.CommandLine/Parsing/SymbolLookupByName.cs b/src/System.CommandLine/Parsing/SymbolLookupByName.cs index ccbacccc3c..eab4c8ac99 100644 --- a/src/System.CommandLine/Parsing/SymbolLookupByName.cs +++ b/src/System.CommandLine/Parsing/SymbolLookupByName.cs @@ -20,7 +20,7 @@ namespace System.CommandLine.Parsing; /// public class SymbolLookupByName { - private class CommandCache(CliCommand command) + private readonly struct CommandCache(CliCommand command) { public CliCommand Command { get; } = command; public Dictionary SymbolsByName { get; } = new(); @@ -62,15 +62,15 @@ private List BuildCache(ParseResult parseResult) return cache; - static void AddSymbolsToCache(CommandCache CommandCache, IEnumerable symbols, CliCommand command) + static void AddSymbolsToCache(CommandCache commandCache, IEnumerable symbols, CliCommand command) { foreach (var symbol in symbols) { - if (CommandCache.SymbolsByName.ContainsKey(symbol.Name)) + if (commandCache.SymbolsByName.ContainsKey(symbol.Name)) { throw new InvalidOperationException($"Command {command.Name} has more than one child named \"{symbol.Name}\"."); } - CommandCache.SymbolsByName.Add(symbol.Name, symbol); + commandCache.SymbolsByName.Add(symbol.Name, symbol); } } } @@ -133,15 +133,35 @@ private bool TryGetSymbolAndParentInternal(string name, /// Gets the symbol with the requested name that appears nearest to the starting command, which defaults to the current or leaf command. /// /// The name to search for + /// An out parameter to receive the symbol, if found. + /// An out parameter to receive the parent, if found. /// The command to start searching up from, which defaults to the current command. /// If true, only the starting command and no ancestors are searched. /// If true, commands are ignored and only options and arguments are found. /// A tuple of the found symbol and its parent command. Throws if the name is not found. /// Thrown if the name is not found. - public (CliSymbol symbol, CliCommand parent) GetSymbolAndParent(string name, CliCommand? startCommand = null, bool skipAncestors = false, bool valuesOnly = false) - => TryGetSymbolAndParentInternal(name, out var symbol, out var parent, out var errorMessage, startCommand, skipAncestors, valuesOnly) - ? (symbol, parent) - : throw new InvalidOperationException(errorMessage); + // TODO: Add tests + public bool TryGetSymbolAndParent(string name, + [NotNullWhen(true)] out CliSymbol? symbol, + [NotNullWhen(true)] out CliCommand? parent, + CliCommand? startCommand = null, + bool skipAncestors = false, + bool valuesOnly = false) + { + if (TryGetSymbolAndParentInternal(name, out var storedSymbol, out var storedParent, out var errorMessage, startCommand, skipAncestors, valuesOnly)) + { + symbol = storedSymbol; + parent = storedParent; + return true; + } + if (errorMessage is not null) + { + throw new InvalidOperationException(errorMessage); + } + symbol = null; + parent = null; + return false; + } /// @@ -159,17 +179,12 @@ public bool TryGetSymbol(string name, [NotNullWhen(true)] out CliSymbol? symbol, private IEnumerable? GetCommandCachesToUse(CliCommand currentCommand) { - if (cache[0].Command == currentCommand) - { - return cache; - } - for (int i = 1; i < cache.Count; i++) // we tested for 0 earlier - { - if (cache[i].Command == currentCommand) - { - return cache.Skip(i); - } - } - return null; + int index = FindIndex(cache, currentCommand); + return index == -1 + ? null + : cache.Skip(index); + + static int FindIndex(List cache, CliCommand? currentCommand) + => cache.FindIndex(c => c.Command == currentCommand); } } diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index eb281aad3a..74f2501a4d 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -105,4 +105,4 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com */ } } -} \ No newline at end of file +} diff --git a/src/System.CommandLine/Parsing/ValueResult.cs b/src/System.CommandLine/Parsing/ValueResult.cs index bc2bec25f2..04cc3114b9 100644 --- a/src/System.CommandLine/Parsing/ValueResult.cs +++ b/src/System.CommandLine/Parsing/ValueResult.cs @@ -22,6 +22,7 @@ private ValueResult( Value = value; Locations = locations; Outcome = outcome; + // TODO: Probably a collection of errors here Error = error; } @@ -40,8 +41,8 @@ internal ValueResult( ValueResultOutcome outcome, // TODO: Error should be an Enumerable and perhaps should not be here at all, only on ParseResult string? error = null) - :this((CliSymbol)argument, value, locations, outcome,error) - { } + : this((CliSymbol)argument, value, locations, outcome, error) + { } /// /// Creates a new ValueResult instance @@ -97,7 +98,7 @@ internal ValueResult( public string? Error { get; } /// - /// Returns test suitable for display. + /// Returns text suitable for display. /// /// /// @@ -106,17 +107,28 @@ public IEnumerable TextForDisplay() throw new NotImplementedException(); } + /// + /// Retrieve the portion of the user's entry that was used for this ValuResult. + /// + /// The text the user entered that resulted in this ValueResult. + /// public IEnumerable TextForCommandReconstruction() { + // TODO: Write method to retrieve from location. throw new NotImplementedException(); } + /// + /// Retrieve the portion of the user's entry that was used for this ValueResult. + /// + /// The text the user entered that resulted in this ValueResult. + /// public override string ToString() - //=> $"{nameof(ValueResult)} ({FormatOutcomeMessage()}) {ValueSymbol?.Name}"; => $"{nameof(ArgumentResult)} {ValueSymbol.Name}: {string.Join(" ", TextForDisplay())}"; - - // TODO: This definitely feels like the wrong place for this, (Some completion stuff was stripped out. This was a private method in ArgumentConversionResult + + // TODO: This might not be the right place for this, (Some completion stuff was stripped out. This was a private method in ArgumentConversionResult) + /* private string FormatOutcomeMessage() => ValueSymbol switch { @@ -135,4 +147,5 @@ private Type ValueSymbolType CliOption option => option.Argument.ValueType, _ => throw new NotImplementedException() }; + */ } From 662ec29ad7ad06162ff269fa65c9e0d8ad4a19c4 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 11 May 2024 02:10:12 -0400 Subject: [PATCH 085/150] Switch to storing annotations in a static field Although it's kind of icky to store instance data on a static field, it is implemented in a robust manner that should prevent any surprises, and the details are hidden from authors of CLIs and authors of subsystems. A subsystem reference is no longer needed when annotating the CliSymbol objects in the grammar, which makes construction of a pipleline much simpler. The fluent grammar construction API now looks like this: ```csharp new Option("--greeting").WithDescription("The greeting") ``` Eventually we will be able to use extension properties: ```csharp new Option("--greeting") { Description = "The greeting" } ``` We still support the `IAnnotationProvider` model that allows advanced CLI authors to lazily or dynamically provide annotations. --- .../AlternateSubsystems.cs | 5 +- .../PipelineTests.cs | 2 +- .../ValueSubsystemTests.cs | 4 +- .../HelpAnnotationExtensions.cs | 25 +++ .../HelpSubsystem.cs | 3 - .../Annotations/AnnotationAccessor.cs | 32 ---- .../{ => Annotations}/AnnotationId.cs | 4 +- ...tionStorageExtensions.AnnotationStorage.cs | 40 +++++ .../AnnotationStorageExtensions.cs | 142 ++++++++++++++++++ .../Annotations/ValueAnnotationAccessor.cs | 67 --------- .../Annotations/ValueAnnotations.cs | 15 +- .../ValueFuncAnnotationAccessor.cs | 58 ------- .../Subsystems/CliSubsystem.cs | 65 ++------ .../Subsystems/DefaultAnnotationProvider.cs | 40 ----- .../Subsystems/IAnnotationProvider.cs | 1 + .../SymbolAnnotationExtensions.cs | 8 +- .../ValueAnnotationExtensions.cs | 89 +++++++++++ .../ValueSubsystem.cs | 22 +-- 18 files changed, 333 insertions(+), 289 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs delete mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs rename src/System.CommandLine.Subsystems/Subsystems/{ => Annotations}/AnnotationId.cs (87%) create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs delete mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs delete mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs delete mode 100644 src/System.CommandLine.Subsystems/Subsystems/DefaultAnnotationProvider.cs create mode 100644 src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index 31679bbef5..15bcf5c112 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Directives; +using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine.Subsystems.Tests { @@ -30,9 +31,7 @@ public VersionThatUsesHelpData(CliSymbol symbol) protected override CliExit Execute(PipelineContext pipelineContext) { - var help = pipelineContext.Pipeline.Help ?? throw new InvalidOperationException("Help cannot be null for this subsystem to work"); - help.Description.TryGet(Symbol, out var description); - + TryGetAnnotation(Symbol, HelpAnnotations.Description, out string? description); pipelineContext.ConsoleHack.WriteLine(description); pipelineContext.AlreadyHandled = true; return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); diff --git a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs index 396da0181f..27308a20e4 100644 --- a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs @@ -186,7 +186,7 @@ public void Subsystems_can_access_each_others_data() if (pipeline.Help is null) throw new InvalidOperationException(); var rootCommand = new CliRootCommand { - symbol.With(pipeline.Help.Description, "Testing") + symbol.WithDescription("Testing") }; pipeline.Execute(new CliConfiguration(rootCommand), "-v", console); diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index 4b724996c9..1db461c0b1 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -61,7 +61,7 @@ public void ValueSubsystem_returns_default_value_when_no_value_is_entered() var configuration = new CliConfiguration(rootCommand); var pipeline = Pipeline.CreateEmpty(); pipeline.Value = new ValueSubsystem(); - pipeline.Value.DefaultValue.Set(option, 43); + option.SetDefaultValue(43); const int expected = 43; var input = $""; @@ -81,7 +81,7 @@ public void ValueSubsystem_returns_calculated_default_value_when_no_value_is_ent var pipeline = Pipeline.CreateEmpty(); pipeline.Value = new ValueSubsystem(); var x = 42; - pipeline.Value.DefaultValueCalculation.Set(option, () => x + 2); + option.SetDefaultValueCalculation(() => x + 2); const int expected = 44; var input = ""; diff --git a/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs new file mode 100644 index 0000000000..1babe85022 --- /dev/null +++ b/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs @@ -0,0 +1,25 @@ +// 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.Subsystems.Annotations; + +namespace System.CommandLine; + +public static class HelpAnnotationExtensions +{ + public static TSymbol WithDescription (this TSymbol symbol, string description) where TSymbol : CliSymbol + { + symbol.SetDescription(description); + return symbol; + } + + public static void SetDescription(this TSymbol symbol, string description) where TSymbol : CliSymbol + { + symbol.SetAnnotation(HelpAnnotations.Description, description); + } + + public static string? GetDescription(this TSymbol symbol) where TSymbol : CliSymbol + { + return symbol.GetAnnotationOrDefault(HelpAnnotations.Description); + } +} diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index a8fe5ad655..65f0b46b65 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -24,9 +24,6 @@ public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) Arity = ArgumentArity.Zero }; - public AnnotationAccessor Description - => new(this, HelpAnnotations.Description); - protected internal override CliConfiguration Initialize(InitializationContext context) { context.Configuration.RootCommand.Add(HelpOption); diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs deleted file mode 100644 index a6a0abb906..0000000000 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs +++ /dev/null @@ -1,32 +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.CodeAnalysis; - -namespace System.CommandLine.Subsystems.Annotations; - -/// -/// Allows associating an annotation with a . The annotation will be stored by the accessor's owner . -/// -public struct AnnotationAccessor(CliSubsystem owner, AnnotationId id) -{ - /// - /// The identifier for this annotation, since subsystems may have multiple annotations. - /// - public AnnotationId Id { get; } = id; - - /// - /// Store a value for the annotation and symbol - /// - /// The CliSymbol the value is for. - /// The value to store. - public readonly void Set(CliSymbol symbol, TValue value) => owner.SetAnnotation(symbol, Id, value); - - /// - /// Retrieve the value for the annotation and symbol - /// - /// The CliSymbol the value is for. - /// The value to retrieve. - /// True if the value was found, false otherwise. - public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, Id, out value); -} diff --git a/src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationId.cs similarity index 87% rename from src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs rename to src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationId.cs index 383214b42a..28928e90e1 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/AnnotationId.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationId.cs @@ -1,7 +1,7 @@ // 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.Subsystems; +namespace System.CommandLine.Subsystems.Annotations; /// /// Describes the ID and type of an annotation. @@ -10,3 +10,5 @@ public record struct AnnotationId(string Prefix, string Id) { public override readonly string ToString() => $"{Prefix}.{Id}"; } + + diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs new file mode 100644 index 0000000000..f15256e603 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs @@ -0,0 +1,40 @@ +// 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.CodeAnalysis; + +namespace System.CommandLine.Subsystems.Annotations; + +partial class AnnotationStorageExtensions +{ + class AnnotationStorage : IAnnotationProvider + { + record struct AnnotationKey(CliSymbol symbol, string annotationId); + + readonly Dictionary annotations = []; + + public bool TryGet(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out TValue? value) + { + if (annotations.TryGetValue(new AnnotationKey(symbol, id.Id), out var obj)) + { + value = (TValue)obj; + return true; + } + + value = default; + return false; + } + + public void Set(CliSymbol symbol, AnnotationId id, TValue value) + { + if (value is not null) + { + annotations[new AnnotationKey(symbol, id.Id)] = value; + } + else + { + annotations.Remove(new AnnotationKey(symbol, id.Id)); + } + } + } +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs new file mode 100644 index 0000000000..5b1d8b54ae --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs @@ -0,0 +1,142 @@ +// 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.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.CommandLine.Subsystems.Annotations; + +/// +/// Handles storage of annotations associated with instances. +/// +public static partial class AnnotationStorageExtensions +{ + // CliSymbol does not offer any PropertyBag-like storage of arbitrary annotations, so the only way to allow setting + // subsystem-specific annotations on CliSymbol instances (such as help description, default value, etc) via simple + // extension methods is to use a static field with a dictionary that associates annotations with CliSymbol instances. + // + // Using ConditionalWeakTable for this dictionary ensures that the symbols and annotations can be collected when the + // symbols are no longer reachable. Although this is unlikely to happen in a CLI app, it is important not to create + // unexpected, unfixable, unbounded memory leaks in apps that construct multiple grammars/pipelines. + // + // The main use case for System.CommandLine is for a CLI app to construct a single annotated grammar in its entry point, + // construct a pipeline using that grammar, and use the pipeline/grammar only once to parse its arguments. However, it + // is important to have well defined and reasonable threading behavior so that System.CommandLine does not behave in + // surprising ways when used in more advanced cases: + // + // * There may be multiple threads constructing and using completely independent grammars/pipelines. This happens in + // our own unit tests, but might happen e.g. in a multithreaded data processing app or web service that uses + // System.CommandLine to process inputs. + // + // * The grammar/pipeline are reentrant; they do not store they do not store internal state, and may be used to parse + // input multiple times. As this is the case, it is reasonable to expect a grammar/pipeline instance to be + // constructed in one thread then used in multiple threads. This might be done by the aforementioned web service or + // data processing app. + // + // The thread-safe behavior of ConditionalWeakTable ensures this works as expected without us having to worry about + // taking locks directly, even though the instance is on a static field and shared between all threads. Note that + // thread local storage is not useful for this, as that would create unexpected behaviors where a grammar constructed + // in one thread would be missing its annotations when used in another thread. + // + // However, while getting values from ConditionalWeakTable is lock free, setting values internally uses an expensive + // lock, so it is not ideal to store all individual annotations directly in the ConditionalWeakTable. This is especially + // true as we do not want the common case of the CLI app entrypoint to have its performance impacted by multithreading + // support more than absolutely necessary. + // + // Instead, we have a single static ConditionalWeakTable that maps each CliSymbol to an AnnotationStorage dictionary, + // which is lazily created and added to the ConditionalWeakTable a single time for each CliSymbol. The individual + // annotations are stored in the AnnotationStorage dictionary, which uses no locks, so is fast, but is not safe to be + // modified from multiple threads. + // + // This is fine, as we will have the following well-defined threading behavior: an annotated grammar and pipeline may + // only be constructed/modified from a single thread. Once the grammar/pipeline instance is fully constructed, it may + // be safely used from multiple threads. + + static readonly ConditionalWeakTable symbolToAnnotationStorage = new(); + + /// + /// Sets the value for the annotation associated with the in the internal annotation storage. + /// + /// The type of the annotation value + /// The symbol that is annotated + /// + /// The identifier for the annotation. For example, the annotation identifier for the help description is . + /// + /// The annotation value + public static void SetAnnotation(this CliSymbol symbol, AnnotationId annotationId, TValue value) + { + var storage = symbolToAnnotationStorage.GetValue(symbol, static (CliSymbol _) => new AnnotationStorage()); + storage.Set(symbol, annotationId, value); + } + + /// + /// Sets the value for the annotation associated with the in the internal annotation storage, + /// and returns the to enable fluent construction of symbols with annotations. + /// + /// The type of the annotation value + /// The symbol that is annotated + /// + /// The identifier for the annotation. For example, the annotation identifier for the help description is . + /// + /// The annotation value + public static TSymbol WithAnnotation(this TSymbol symbol, AnnotationId annotationId, TValue value) where TSymbol : CliSymbol + { + symbol.SetAnnotation(annotationId, value); + return symbol; + } + + /// + /// Attempts to get the value for the annotation associated with the , + /// first from the optional , and falling back to the internal annotation storage used to + /// store values set via . + /// + /// The type of the annotation value + /// The symbol that is annotated + /// + /// The identifier for the annotation. For example, the annotation identifier for the help description is . + /// + /// The annotation value, if successful, otherwise default + /// + /// An optional annotation provider that may implement custom or lazy construction of annotation values. Annotation returned by an annotation + /// provider take precedence over those stored in internal annotation storage. + /// + /// True if successful + public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value, IAnnotationProvider? provider = null) + { + if (provider is not null && provider.TryGet(symbol, annotationId, out value)) + { + return true; + } + + if (symbolToAnnotationStorage.TryGetValue(symbol, out var storage) && storage.TryGet (symbol, annotationId, out value)) + { + return true; + } + + value = default; + return false; + } + /// + /// Attempt to retrieve the 's value for the annotation + /// from the optional and the internal annotation storage. + /// + /// The type of the annotation value + /// The symbol that is annotated + /// + /// The identifier for the annotation. For example, the annotation identifier for the help description is . + /// + /// + /// An optional annotation provider that may implement custom or lazy construction of annotation values. Annotation returned by an annotation + /// provider take precedence over those stored in internal annotation storage. + /// + /// The annotation value, if successful, otherwise default + public static TValue? GetAnnotationOrDefault(this CliSymbol symbol, AnnotationId annotationId, IAnnotationProvider? provider = null) + { + if (symbol.TryGetAnnotation(annotationId, out TValue? value, provider)) + { + return value; + } + + return default; + } +} \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs deleted file mode 100644 index 613ce48c7f..0000000000 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotationAccessor.cs +++ /dev/null @@ -1,67 +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.CodeAnalysis; - -namespace System.CommandLine.Subsystems.Annotations; - -/// -/// Associates an annotation with a . The symbol must be an option or argument and the value must be of the same type as the symbol./>. -/// -/// -/// The annotation will be stored by the accessor's owner . -/// -/// The type of value to be stored -/// The subsystem that this annotation store data for. -/// The identifier for this annotation, since subsystems may have multiple annotations. -public struct ValueAnnotationAccessor(CliSubsystem owner, AnnotationId id) -{ - /// > - public AnnotationId Id { get; } - - /// > - public readonly void Set(CliOption symbol, TSymbolValue value) - where TSymbolValue : TValue - => owner.SetAnnotation(symbol, id, value); - - /// > - public readonly void Set(CliArgument symbol, TSymbolValue value) - where TSymbolValue : TValue - => owner.SetAnnotation(symbol, id, value); - - // TODO: Consider whether we need a version that takes a CliSymbol (ValueSymbol) - /// > - public readonly bool TryGet(CliOption symbol, [NotNullWhen(true)] out TValue? value) - where TSymbolValue : TValue - => TryGetInternal(symbol, out value); - - /// > - public readonly bool TryGet(CliArgument symbol, [NotNullWhen(true)] out TValue? value) - where TSymbolValue : TValue - => TryGetInternal(symbol, out value); - - /// > - /// - /// This overload will throw if the stored value cannot be converted to the type. - /// - /// - public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) - where TSymbolValue : TValue - => TryGetInternal(symbol, out value); - - private readonly bool TryGetInternal(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) - where TSymbolValue : TValue - { - if (owner.TryGetAnnotation(symbol, id, out var storedValue)) - { - if (storedValue is TSymbolValue symbolValue) - { - value = symbolValue; - return true; - } - throw new ArgumentException("The requested type is incorrect.", nameof(symbol)); - } - value = default; - return false; - } -} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs index 8a2737cdf5..466f34f2b4 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs @@ -11,12 +11,21 @@ public static class ValueAnnotations internal static string Prefix { get; } = nameof(SubsystemKind.Value); /// - /// Provides Set and Get for default values + /// Default value for an option or argument /// + /// + /// Although the type is , it must actually be the same type as the type + /// parameter of the or . + /// public static AnnotationId DefaultValue { get; } = new(Prefix, nameof(DefaultValue)); /// - /// Provides Set and Get for default value calculations + /// Default value calculation for an option or argument /// - public static AnnotationId> DefaultValueCalculation { get; } = new(Prefix, nameof(DefaultValueCalculation)); + /// + /// Although the type is , it must actually be a + /// with a type parameter matching the the type parameter type of the + /// or + /// + public static AnnotationId DefaultValueCalculation { get; } = new(Prefix, nameof(DefaultValueCalculation)); } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs deleted file mode 100644 index e30ec57885..0000000000 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueFuncAnnotationAccessor.cs +++ /dev/null @@ -1,58 +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.CodeAnalysis; - -namespace System.CommandLine.Subsystems.Annotations; - -/// -/// Associates an annotation with a . The symbol must be an option or argument and the delegate must return a value of the same type as the symbol. -/// -/// -/// The annotation will be stored by the accessor's owner . -/// -/// The type of value to be stored -/// The subsystem that this annotation store data for. -/// The identifier for this annotation, since subsystems may have multiple annotations. -// TODO: If we keep this approach, consider making this class more general purpose or replacing use with AnnotationAccessior -public struct ValueFuncAnnotationAccessor(CliSubsystem owner, AnnotationId> id) -{ - /// > - public AnnotationId> Id { get; } - - /// > - public readonly void Set(CliOption symbol, Func value) - => owner.SetAnnotation(symbol, id, value); - - /// > - public readonly void Set(CliArgument symbol, Func value) - => owner.SetAnnotation(symbol, id, value); - - // TODO: Consider whether we need a version that takes a CliSymbol (ValueSymbol) - /// > - public readonly bool TryGet(CliOption symbol, [NotNullWhen(true)] out Func? value) - => TryGetInternal(symbol, out value); - - /// > - public readonly bool TryGet(CliArgument symbol, [NotNullWhen(true)] out Func? value) - => TryGetInternal(symbol, out value); - - /// > - /// - /// This overload will throw if the stored value cannot be converted to the type. - /// - /// - public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out Func? value) - => TryGetInternal(symbol, out value); - - private readonly bool TryGetInternal(CliSymbol symbol, [NotNullWhen(true)] out Func? value) - { - if (owner.TryGetAnnotation(symbol, id, out Func? storedValue)) - { - value = storedValue; - return true; - } - value = default; - return false; - } -} diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 25ac7bd213..f280e809d3 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -1,6 +1,7 @@ // 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.Subsystems.Annotations; using System.Diagnostics.CodeAnalysis; namespace System.CommandLine.Subsystems; @@ -28,74 +29,26 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv /// public SubsystemKind SubsystemKind { get; } - private DefaultAnnotationProvider? _defaultProvider; private readonly IAnnotationProvider? _annotationProvider; /// - /// Attempt to retrieve the value for the symbol and annotation ID from the annotation provider. + /// Attempt to retrieve the 's value for the annotation . This will check the + /// annotation provider that was passed to the subsystem constructor, and the internal annotation storage. /// /// The value of the type to retrieve /// The symbol the value is attached to - /// The id for the value to be retrieved. For example, an annotation ID for help is description + /// + /// The identifier for the annotation value to be retrieved. + /// For example, the annotation identifier for the help description is . + /// /// An out parameter to contain the result /// True if successful /// - /// This value is protected because these values are always retrieved from derived classes that offer - /// strongly typed explicit methods, such as help having `GetDescription(Symbol symbol)` method. + /// This value is protected because it is a convenience for subsystem authors. It calls /// protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out TValue? value) { - if (_defaultProvider is not null && _defaultProvider.TryGet(symbol, id, out value)) - { - return true; - } - if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, id, out value)) - { - return true; - } - value = default; - return false; - } - - /// - /// A delegate that returns the value. - protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId> id, [NotNullWhen(true)] out Func? value) - { - if (_defaultProvider is not null && _defaultProvider.TryGet(symbol, id, out var storedValue)) - { - value = storedValue; - return true; - } - if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, id, out var storedValue2)) - { - value = storedValue2; - return true; - } - value = default; - return false; - } - - /// - /// Set the value for the symbol and annotation ID in the annotation provider. - /// - /// The value of the type to set - /// The symbol the value is attached to - /// The id for the value to be set. For example, an annotation ID for help is description - /// An out parameter to contain the result - /// - /// This value is protected because these values are always retrieved from derived classes that offer - /// strongly typed explicit methods, such as help having `GetDescription(Symbol symbol, "My help description")` method. - /// - protected internal void SetAnnotation(CliSymbol symbol, AnnotationId id, TValue value) - { - (_defaultProvider ??= new DefaultAnnotationProvider()).Set(symbol, id, value); - } - - /// - /// A delegate that returns the value. - protected internal void SetAnnotation(CliSymbol symbol, AnnotationId> id, Func value) - { - (_defaultProvider ??= new DefaultAnnotationProvider()).Set>(symbol, id, value); + return symbol.TryGetAnnotation(id, out value, _annotationProvider); } /// diff --git a/src/System.CommandLine.Subsystems/Subsystems/DefaultAnnotationProvider.cs b/src/System.CommandLine.Subsystems/Subsystems/DefaultAnnotationProvider.cs deleted file mode 100644 index 0d8ec37dbc..0000000000 --- a/src/System.CommandLine.Subsystems/Subsystems/DefaultAnnotationProvider.cs +++ /dev/null @@ -1,40 +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.CodeAnalysis; - -namespace System.CommandLine.Subsystems; - -/// -/// Default storage for annotations -/// -class DefaultAnnotationProvider : IAnnotationProvider -{ - record struct AnnotationKey(CliSymbol symbol, string annotationId); - - readonly Dictionary annotations = []; - - public bool TryGet(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out TValue? value) - { - if (annotations.TryGetValue(new AnnotationKey(symbol, id.Id), out var obj)) - { - value = (TValue)obj; - return true; - } - - value = default; - return false; - } - - public void Set(CliSymbol symbol, AnnotationId id, TValue value) - { - if (value is not null) - { - annotations[new AnnotationKey(symbol, id.Id)] = value; - } - else - { - annotations.Remove(new AnnotationKey(symbol, id.Id)); - } - } -} diff --git a/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs b/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs index 4885b1623f..8b3c4203ac 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs @@ -1,6 +1,7 @@ // 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.Subsystems.Annotations; using System.Diagnostics.CodeAnalysis; namespace System.CommandLine.Subsystems; diff --git a/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs index dd01d767a3..53e2fb5117 100644 --- a/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs @@ -1,6 +1,7 @@ // 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.Subsystems; using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine; @@ -10,13 +11,6 @@ namespace System.CommandLine; /// public static class SymbolAnnotationExtensions { - public static TSymbol With(this TSymbol symbol, AnnotationAccessor annotation, TValue value) - where TSymbol : CliSymbol - { - annotation.Set(symbol, value); - return symbol; - } - public static TCommand With(this TCommand command, CliCommand subcommand) where TCommand : CliCommand { diff --git a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs new file mode 100644 index 0000000000..4966024885 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs @@ -0,0 +1,89 @@ +// 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.Subsystems.Annotations; + +namespace System.CommandLine; + +public static class ValueAnnotationExtensions +{ + public static CliOption WithDefaultValue (this CliOption option, TValue defaultValue) + { + option.SetDefaultValue(defaultValue); + return option; + } + + public static void SetDefaultValue(this CliOption option, TValue defaultValue) + { + option.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); + } + + public static TValue? GetDefaultValue(this CliOption option) + { + if (option.TryGetAnnotation(ValueAnnotations.DefaultValue, out var defaultValue)) + { + return (TValue?)defaultValue; + } + return default; + } + + public static CliArgument WithDefaultValue(this CliArgument argument, TValue defaultValue) + { + argument.SetDefaultValue(defaultValue); + return argument; + } + + public static void SetDefaultValue(this CliArgument argument, TValue defaultValue) + { + argument.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); + } + + public static TValue? GetDefaultValue(this CliArgument argument) + { + if (argument.TryGetAnnotation(ValueAnnotations.DefaultValue, out var defaultValue)) + { + return (TValue?)defaultValue; + } + return default; + } + + public static CliOption WithDefaultValueCalculation(this CliOption option, Func defaultValueCalculation) + { + option.SetDefaultValueCalculation(defaultValueCalculation); + return option; + } + + public static void SetDefaultValueCalculation(this CliOption option, Func defaultValueCalculation) + { + option.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); + } + + public static Func? GetDefaultValueCalculation(this CliOption option) + { + if (option.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out var defaultValueCalculation)) + { + return (Func)defaultValueCalculation; + } + return default; + } + + public static CliArgument WithDefaultValueCalculation(this CliArgument argument, Func defaultValueCalculation) + { + argument.SetDefaultValueCalculation(defaultValueCalculation); + return argument; + } + + public static void SetDefaultValueCalculation(this CliArgument argument, Func defaultValueCalculation) + { + argument.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); + } + + public static Func? GetDefaultValueCalculation(this CliArgument argument) + { + if (argument.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out var defaultValueCalculation)) + { + return (Func)defaultValueCalculation; + } + return default; + } +} diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index bf3f650d94..a071dd550c 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -12,18 +12,6 @@ public class ValueSubsystem(IAnnotationProvider? annotationProvider = null) private Dictionary cachedValues = []; private ParseResult? parseResult = null; - /// - /// Provides access to Get and Set methods for default values for symbols - /// - public ValueAnnotationAccessor DefaultValue - => new(this, ValueAnnotations.DefaultValue); - - /// - /// Provides access to Get and Set methods for default value calculations for symbols - /// - public ValueFuncAnnotationAccessor DefaultValueCalculation - => new(this, ValueAnnotations.DefaultValueCalculation); - // It is possible that another subsystems GetIsActivated method will access a value. // If this is called from a GetIsActivated method of a subsystem in the early termination group, // it will fail. That is not an expected scenario. @@ -72,6 +60,8 @@ private bool TryGetValue(CliSymbol symbol, out T? value) private T? GetValueInternal(CliSymbol? symbol) { + // NOTE: We use the subsystem's TryGetAnnotation here instead of the GetDefaultValue etc + // extension methods, as the subsystem's TryGetAnnotation respects its annotation provider return symbol switch { not null when TryGetValue(symbol, out var value) @@ -84,9 +74,9 @@ not null when TryGetValue(symbol, out var value) // configuration values go here in precedence //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) // => UseValue(symbol, GetEnvByName(envName)), - not null when DefaultValueCalculation.TryGet(symbol, out var defaultValueCalculation) - => UseValue(symbol, CalculatedDefault(symbol, defaultValueCalculation)), - not null when DefaultValue.TryGet(symbol, out var explicitValue) + not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValueCalculation, out var defaultValueCalculation) + => UseValue(symbol, CalculatedDefault(symbol, (Func) defaultValueCalculation)), + not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var explicitValue) => UseValue(symbol, (T)explicitValue), null => throw new ArgumentNullException(nameof(symbol)), _ => UseValue(symbol, default(T)) @@ -99,7 +89,7 @@ not null when DefaultValue.TryGet(symbol, out var explicitValue) } } - private static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) + private static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) { var objectValue = defaultValueCalculation(); var value = objectValue is null From b28ce9240075fbd465f1ff04ac60a517c9db1b5b Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 11 May 2024 17:49:43 -0400 Subject: [PATCH 086/150] Clarify the new annotation mechanism --- .../HelpAnnotationExtensions.cs | 25 +++++ .../HelpSubsystem.cs | 3 + ...tionStorageExtensions.AnnotationStorage.cs | 17 ++- .../AnnotationStorageExtensions.cs | 54 ++++----- .../Subsystems/CliSubsystem.cs | 12 +- .../ValueAnnotationExtensions.cs | 103 +++++++++++++++++- 6 files changed, 177 insertions(+), 37 deletions(-) diff --git a/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs index 1babe85022..57e9c771f1 100644 --- a/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs @@ -1,23 +1,48 @@ // 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.Subsystems; using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine; public static class HelpAnnotationExtensions { + /// + /// Sets the help description on the + /// + /// The type of the symbol + /// The symbol + /// The help description for the symbol + /// The , to enable fluent construction of symbols with annotations. public static TSymbol WithDescription (this TSymbol symbol, string description) where TSymbol : CliSymbol { symbol.SetDescription(description); return symbol; } + + /// + /// Sets the help description on the + /// + /// The type of the symbol + /// The symbol + /// The help description for the symbol public static void SetDescription(this TSymbol symbol, string description) where TSymbol : CliSymbol { symbol.SetAnnotation(HelpAnnotations.Description, description); } + /// + /// Get the help description on the + /// + /// The type of the symbol + /// The symbol + /// The symbol description if any, otherwise + /// + /// This is intended to be called by CLI authors. Subsystems should instead call , + /// values from the subsystem's . + /// public static string? GetDescription(this TSymbol symbol) where TSymbol : CliSymbol { return symbol.GetAnnotationOrDefault(HelpAnnotations.Description); diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index 65f0b46b65..76b373d723 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -40,4 +40,7 @@ protected internal override CliExit Execute(PipelineContext pipelineContext) pipelineContext.ConsoleHack.WriteLine("Help me!"); return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); } + + public bool TryGetDescription (CliSymbol symbol, out string? description) + => TryGetAnnotation (symbol, HelpAnnotations.Description, out description); } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs index f15256e603..5123f11d20 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs @@ -9,13 +9,17 @@ partial class AnnotationStorageExtensions { class AnnotationStorage : IAnnotationProvider { - record struct AnnotationKey(CliSymbol symbol, string annotationId); + record struct AnnotationKey(CliSymbol symbol, string prefix, string id) + { + public static AnnotationKey Create (CliSymbol symbol, AnnotationId annotationId) + => new (symbol, annotationId.Prefix, annotationId.Id); + } readonly Dictionary annotations = []; - public bool TryGet(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out TValue? value) + public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) { - if (annotations.TryGetValue(new AnnotationKey(symbol, id.Id), out var obj)) + if (annotations.TryGetValue(AnnotationKey.Create(symbol, annotationId), out var obj)) { value = (TValue)obj; return true; @@ -25,15 +29,16 @@ public bool TryGet(CliSymbol symbol, AnnotationId id, [NotNullWh return false; } - public void Set(CliSymbol symbol, AnnotationId id, TValue value) + public void Set(CliSymbol symbol, AnnotationId annotationId, TValue value) { + var key = AnnotationKey.Create(symbol, annotationId); if (value is not null) { - annotations[new AnnotationKey(symbol, id.Id)] = value; + annotations[key] = value; } else { - annotations.Remove(new AnnotationKey(symbol, id.Id)); + annotations.Remove(key); } } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs index 5b1d8b54ae..fff6373677 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs @@ -50,7 +50,7 @@ public static partial class AnnotationStorageExtensions // // This is fine, as we will have the following well-defined threading behavior: an annotated grammar and pipeline may // only be constructed/modified from a single thread. Once the grammar/pipeline instance is fully constructed, it may - // be safely used from multiple threads. + // be safely used from multiple threads, but is not safe to further modify once in use. static readonly ConditionalWeakTable symbolToAnnotationStorage = new(); @@ -68,7 +68,7 @@ public static void SetAnnotation(this CliSymbol symbol, AnnotationId new AnnotationStorage()); storage.Set(symbol, annotationId, value); } - + /// /// Sets the value for the annotation associated with the in the internal annotation storage, /// and returns the to enable fluent construction of symbols with annotations. @@ -79,6 +79,9 @@ public static void SetAnnotation(this CliSymbol symbol, AnnotationId. /// /// The annotation value + /// + /// The , to enable fluent construction of symbols with annotations. + /// public static TSymbol WithAnnotation(this TSymbol symbol, AnnotationId annotationId, TValue value) where TSymbol : CliSymbol { symbol.SetAnnotation(annotationId, value); @@ -86,28 +89,24 @@ public static TSymbol WithAnnotation(this TSymbol symbol, Annot } /// - /// Attempts to get the value for the annotation associated with the , - /// first from the optional , and falling back to the internal annotation storage used to - /// store values set via . + /// Attempts to get the value for the annotation associated with the in the internal annotation + /// storage used to store values set via . /// /// The type of the annotation value /// The symbol that is annotated - /// + /// /// The identifier for the annotation. For example, the annotation identifier for the help description is . /// /// The annotation value, if successful, otherwise default - /// - /// An optional annotation provider that may implement custom or lazy construction of annotation values. Annotation returned by an annotation - /// provider take precedence over those stored in internal annotation storage. - /// /// True if successful - public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value, IAnnotationProvider? provider = null) + /// + /// This is intended to be called by specialized ID-specific accessors for CLI authors such as . + /// Subsystems should not call it, as it does not account for values from the subsystem's . They should instead call + /// or an ID-specific accessor on the subsystem such + /// . + /// + public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) { - if (provider is not null && provider.TryGet(symbol, annotationId, out value)) - { - return true; - } - if (symbolToAnnotationStorage.TryGetValue(symbol, out var storage) && storage.TryGet (symbol, annotationId, out value)) { return true; @@ -116,27 +115,30 @@ public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId< value = default; return false; } + /// - /// Attempt to retrieve the 's value for the annotation - /// from the optional and the internal annotation storage. + /// Attempts to get the value for the annotation associated with the in the internal annotation + /// storage used to store values set via . /// /// The type of the annotation value /// The symbol that is annotated - /// + /// /// The identifier for the annotation. For example, the annotation identifier for the help description is . /// - /// - /// An optional annotation provider that may implement custom or lazy construction of annotation values. Annotation returned by an annotation - /// provider take precedence over those stored in internal annotation storage. - /// /// The annotation value, if successful, otherwise default - public static TValue? GetAnnotationOrDefault(this CliSymbol symbol, AnnotationId annotationId, IAnnotationProvider? provider = null) + /// + /// This is intended to be called by specialized ID-specific accessors for CLI authors such as . + /// Subsystems should not call it, as it does not account for values from the subsystem's . They should instead call + /// or an ID-specific accessor on the subsystem such + /// . + /// + public static TValue? GetAnnotationOrDefault(this CliSymbol symbol, AnnotationId annotationId) { - if (symbol.TryGetAnnotation(annotationId, out TValue? value, provider)) + if (symbol.TryGetAnnotation(annotationId, out TValue? value)) { return value; } return default; } -} \ No newline at end of file +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index f280e809d3..e02c6df183 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -44,11 +44,17 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv /// An out parameter to contain the result /// True if successful /// - /// This value is protected because it is a convenience for subsystem authors. It calls + /// Subsystem authors must use this to access annotation values, as it respects the subsystem's if it has one. + /// This value is protected because it is intended for use only by subsystem authors. It calls /// - protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out TValue? value) + protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) { - return symbol.TryGetAnnotation(id, out value, _annotationProvider); + if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, annotationId, out value)) + { + return true; + } + + return symbol.TryGetAnnotation(annotationId, out value); } /// diff --git a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs index 4966024885..2e5b12c277 100644 --- a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs @@ -1,24 +1,49 @@ // 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.Subsystems; using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine; public static class ValueAnnotationExtensions { + /// + /// Sets the default value annotation on the + /// + /// The type of the option value + /// The option + /// The default value for the option + /// The , to enable fluent construction of symbols with annotations. public static CliOption WithDefaultValue (this CliOption option, TValue defaultValue) { option.SetDefaultValue(defaultValue); return option; } + /// + /// Sets the default value annotation on the + /// + /// The type of the option value + /// The option + /// The default value for the option public static void SetDefaultValue(this CliOption option, TValue defaultValue) { option.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); } - public static TValue? GetDefaultValue(this CliOption option) + /// + /// Get the default value annotation for the + /// + /// The type of the option value + /// The option + /// The option's default value annotation if any, otherwise + /// + /// This is intended to be called by CLI authors. Subsystems should instead call , + /// which calculates the actual default value, based on the default value annotation and default value calculation, + /// whether directly stored on the symbol or from the subsystem's . + /// + public static TValue? GetDefaultValueAnnotation(this CliOption option) { if (option.TryGetAnnotation(ValueAnnotations.DefaultValue, out var defaultValue)) { @@ -27,18 +52,43 @@ public static void SetDefaultValue(this CliOption option, TValue return default; } + /// + /// Sets the default value annotation on the + /// + /// The type of the argument value + /// The argument + /// The default value for the argument + /// The , to enable fluent construction of symbols with annotations. public static CliArgument WithDefaultValue(this CliArgument argument, TValue defaultValue) { argument.SetDefaultValue(defaultValue); return argument; } + /// + /// Sets the default value annotation on the + /// + /// The type of the argument value + /// The argument + /// The default value for the argument + /// The , to enable fluent construction of symbols with annotations. public static void SetDefaultValue(this CliArgument argument, TValue defaultValue) { argument.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); } - public static TValue? GetDefaultValue(this CliArgument argument) + /// + /// Get the default value annotation for the + /// + /// The type of the argument value + /// The argument + /// The argument's default value annotation if any, otherwise + /// + /// This is intended to be called by CLI authors. Subsystems should instead call , + /// which calculates the actual default value, based on the default value annotation and default value calculation, + /// whether directly stored on the symbol or from the subsystem's . + /// + public static TValue? GetDefaultValueAnnotation(this CliArgument argument) { if (argument.TryGetAnnotation(ValueAnnotations.DefaultValue, out var defaultValue)) { @@ -47,17 +97,41 @@ public static void SetDefaultValue(this CliArgument argument, TV return default; } + /// + /// Sets the default value calculation for the + /// + /// The type of the option value + /// The option + /// The default value calculation for the option + /// The , to enable fluent construction of symbols with annotations. public static CliOption WithDefaultValueCalculation(this CliOption option, Func defaultValueCalculation) { option.SetDefaultValueCalculation(defaultValueCalculation); return option; } + /// + /// Sets the default value calculation for the + /// + /// The type of the option value + /// The option + /// The default value calculation for the option public static void SetDefaultValueCalculation(this CliOption option, Func defaultValueCalculation) { option.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); } + /// + /// Get the default value calculation for the + /// + /// The type of the option value + /// The option + /// The option's default value calculation if any, otherwise + /// + /// This is intended to be called by CLI authors. Subsystems should instead call , + /// which calculates the actual default value, based on the default value annotation and default value calculation, + /// whether directly stored on the symbol or from the subsystem's . + /// public static Func? GetDefaultValueCalculation(this CliOption option) { if (option.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out var defaultValueCalculation)) @@ -67,17 +141,42 @@ public static void SetDefaultValueCalculation(this CliOption opt return default; } + /// + /// Sets the default value calculation for the + /// + /// The type of the argument value + /// The argument + /// The default value calculation for the argument + /// The , to enable fluent construction of symbols with annotations. public static CliArgument WithDefaultValueCalculation(this CliArgument argument, Func defaultValueCalculation) { argument.SetDefaultValueCalculation(defaultValueCalculation); return argument; } + /// + /// Sets the default value calculation for the + /// + /// The type of the argument value + /// The argument + /// The default value calculation for the argument + /// The , to enable fluent construction of symbols with annotations. public static void SetDefaultValueCalculation(this CliArgument argument, Func defaultValueCalculation) { argument.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); } + /// + /// Get the default value calculation for the + /// + /// The type of the argument value + /// The argument + /// The argument's default value calculation if any, otherwise + /// + /// This is intended to be called by CLI authors. Subsystems should instead call , + /// which calculates the actual default value, based on the default value annotation and default value calculation, + /// whether directly stored on the symbol or from the subsystem's . + /// public static Func? GetDefaultValueCalculation(this CliArgument argument) { if (argument.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out var defaultValueCalculation)) From 6bfb62d88129abd7b3a0bf11b820e34844112b39 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 5 May 2024 08:48:13 -0400 Subject: [PATCH 087/150] Renamed PipelineContext --- .../AlternateSubsystems.cs | 6 +++--- .../VersionSubsystemTests.cs | 8 ++++---- src/System.CommandLine.Subsystems/CliExit.cs | 2 +- .../CompletionSubsystem.cs | 2 +- .../Directives/DiagramSubsystem.cs | 2 +- src/System.CommandLine.Subsystems/HelpSubsystem.cs | 2 +- src/System.CommandLine.Subsystems/Pipeline.cs | 6 +++--- .../Subsystems/CliSubsystem.cs | 6 +++--- .../{PipelineContext.cs => PipelineResult.cs} | 2 +- .../Subsystems/Subsystem.cs | 12 ++++++------ src/System.CommandLine.Subsystems/ValueSubsystem.cs | 2 +- .../VersionSubsystem.cs | 2 +- 12 files changed, 26 insertions(+), 26 deletions(-) rename src/System.CommandLine.Subsystems/Subsystems/{PipelineContext.cs => PipelineResult.cs} (82%) diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index 15bcf5c112..f035c87297 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -10,7 +10,7 @@ internal class AlternateSubsystems { internal class AlternateVersion : VersionSubsystem { - protected override CliExit Execute(PipelineContext pipelineContext) + protected override CliExit Execute(PipelineResult pipelineContext) { pipelineContext.ConsoleHack.WriteLine($"***{CliExecutable.ExecutableVersion}***"); pipelineContext.AlreadyHandled = true; @@ -29,7 +29,7 @@ public VersionThatUsesHelpData(CliSymbol symbol) private CliSymbol Symbol { get; } - protected override CliExit Execute(PipelineContext pipelineContext) + protected override CliExit Execute(PipelineResult pipelineContext) { TryGetAnnotation(Symbol, HelpAnnotations.Description, out string? description); pipelineContext.ConsoleHack.WriteLine(description); @@ -51,7 +51,7 @@ protected override CliConfiguration Initialize(InitializationContext context) return base.Initialize(context); } - protected override CliExit Execute(PipelineContext pipelineContext) + protected override CliExit Execute(PipelineResult pipelineContext) { ExecutionWasRun = true; return base.Execute(pipelineContext); diff --git a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs index 1c7501aceb..bdef1b2daf 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionSubsystemTests.cs @@ -52,7 +52,7 @@ public void Outputs_assembly_version() { var consoleHack = new ConsoleHack().RedirectToBuffer(true); var versionSubsystem = new VersionSubsystem(); - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + Subsystem.Execute(versionSubsystem, new PipelineResult(null, "", null, consoleHack)); consoleHack.GetBuffer().Trim().Should().Be(Constants.version); } @@ -64,7 +64,7 @@ public void Outputs_specified_version() { SpecificVersion = "42" }; - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + Subsystem.Execute(versionSubsystem, new PipelineResult(null, "", null, consoleHack)); consoleHack.GetBuffer().Trim().Should().Be("42"); } @@ -76,7 +76,7 @@ public void Outputs_assembly_version_when_specified_version_set_to_null() { SpecificVersion = null }; - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + Subsystem.Execute(versionSubsystem, new PipelineResult(null, "", null, consoleHack)); consoleHack.GetBuffer().Trim().Should().Be(Constants.version); } @@ -88,7 +88,7 @@ public void Console_output_can_be_tested() var consoleHack = new ConsoleHack().RedirectToBuffer(true); var versionSubsystem = new VersionSubsystem(); - Subsystem.Execute(versionSubsystem, new PipelineContext(null, "", null, consoleHack)); + Subsystem.Execute(versionSubsystem, new PipelineResult(null, "", null, consoleHack)); consoleHack.GetBuffer().Trim().Should().Be(Constants.version); } diff --git a/src/System.CommandLine.Subsystems/CliExit.cs b/src/System.CommandLine.Subsystems/CliExit.cs index 7e2731d865..460461d2cf 100644 --- a/src/System.CommandLine.Subsystems/CliExit.cs +++ b/src/System.CommandLine.Subsystems/CliExit.cs @@ -8,7 +8,7 @@ namespace System.CommandLine; // TODO: Consider what info is needed after invocation. If it's the whole pipeline context, consider collapsing this with that class. public class CliExit { - internal CliExit(PipelineContext pipelineContext) + internal CliExit(PipelineResult pipelineContext) : this(pipelineContext.ParseResult, pipelineContext.AlreadyHandled, pipelineContext.ExitCode) { } diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index adae591c3a..11177b873d 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -18,7 +18,7 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) ? false : false; - protected internal override CliExit Execute(PipelineContext pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineContext) { pipelineContext.ConsoleHack.WriteLine("Not yet implemented"); return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 90cc244382..242e07ffe6 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -13,7 +13,7 @@ public class DiagramSubsystem( IAnnotationProvider? annotationProvider = null) //protected internal override bool GetIsActivated(ParseResult? parseResult) // => parseResult is not null && option is not null && parseResult.GetValue(option); - protected internal override CliExit Execute(PipelineContext pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineContext) { // Gather locations //var locations = pipelineContext.ParseResult.LocationMap diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index 76b373d723..22d5a4b61d 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -34,7 +34,7 @@ protected internal override CliConfiguration Initialize(InitializationContext co protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.GetValue(HelpOption); - protected internal override CliExit Execute(PipelineContext pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineContext) { // TODO: Match testable output pattern pipelineContext.ConsoleHack.WriteLine("Help me!"); diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 93a1ff3c74..7972e3b25c 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -62,7 +62,7 @@ public CliExit Execute(CliConfiguration configuration, string[] args, string raw public CliExit Execute(ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) { - var pipelineContext = new PipelineContext(parseResult, rawInput, this, consoleHack ?? new ConsoleHack()); + var pipelineContext = new PipelineResult(parseResult, rawInput, this, consoleHack ?? new ConsoleHack()); ExecuteSubsystems(pipelineContext); return new CliExit(pipelineContext); } @@ -109,7 +109,7 @@ protected virtual CliExit TearDownSubsystems(CliExit cliExit) return cliExit; } - protected virtual void ExecuteSubsystems(PipelineContext pipelineContext) + protected virtual void ExecuteSubsystems(PipelineResult pipelineContext) { // TODO: Consider redesign where pipelineContext is not modifiable. // @@ -122,7 +122,7 @@ protected virtual void ExecuteSubsystems(PipelineContext pipelineContext) } } - protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineContext pipelineContext) + protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineResult pipelineContext) { if (subsystem is not null && (!pipelineContext.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) { diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index e02c6df183..d92c2396af 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -67,13 +67,13 @@ protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId< /// /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate /// A CliExit object with information such as whether the CLI should terminate - protected internal virtual CliExit Execute(PipelineContext pipelineContext) + protected internal virtual CliExit Execute(PipelineResult pipelineContext) => CliExit.NotRun(pipelineContext.ParseResult); - internal PipelineContext ExecuteIfNeeded(PipelineContext pipelineContext) + internal PipelineResult ExecuteIfNeeded(PipelineResult pipelineContext) => ExecuteIfNeeded(pipelineContext.ParseResult, pipelineContext); - internal PipelineContext ExecuteIfNeeded(ParseResult? parseResult, PipelineContext pipelineContext) + internal PipelineResult ExecuteIfNeeded(ParseResult? parseResult, PipelineResult pipelineContext) { if (GetIsActivated(parseResult)) { diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs b/src/System.CommandLine.Subsystems/Subsystems/PipelineResult.cs similarity index 82% rename from src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs rename to src/System.CommandLine.Subsystems/Subsystems/PipelineResult.cs index 06f12dd84e..00c02bc9ac 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/PipelineContext.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/PipelineResult.cs @@ -3,7 +3,7 @@ namespace System.CommandLine.Subsystems; -public class PipelineContext(ParseResult? parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) +public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) { public ParseResult? ParseResult { get; } = parseResult; public string RawInput { get; } = rawInput; diff --git a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs index f67f5ecdc4..ca99631395 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs @@ -8,22 +8,22 @@ public class Subsystem public static void Initialize(CliSubsystem subsystem, CliConfiguration configuration, IReadOnlyList args) => subsystem.Initialize(new InitializationContext(configuration, args)); - public static CliExit Execute(CliSubsystem subsystem, PipelineContext pipelineContext) + public static CliExit Execute(CliSubsystem subsystem, PipelineResult pipelineContext) => subsystem.Execute(pipelineContext); public static bool GetIsActivated(CliSubsystem subsystem, ParseResult parseResult) => subsystem.GetIsActivated(parseResult); public static CliExit ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) - => new(subsystem.ExecuteIfNeeded(new PipelineContext(parseResult, rawInput, null, consoleHack))); + => new(subsystem.ExecuteIfNeeded(new PipelineResult(parseResult, rawInput, null, consoleHack))); public static CliExit Execute(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) - => subsystem.Execute(new PipelineContext(parseResult, rawInput, null, consoleHack)); + => subsystem.Execute(new PipelineResult(parseResult, rawInput, null, consoleHack)); - internal static PipelineContext ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack, PipelineContext? pipelineContext = null) - => subsystem.ExecuteIfNeeded(pipelineContext ?? new PipelineContext(parseResult, rawInput, null, consoleHack)); + internal static PipelineResult ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack, PipelineResult? pipelineContext = null) + => subsystem.ExecuteIfNeeded(pipelineContext ?? new PipelineResult(parseResult, rawInput, null, consoleHack)); - internal static PipelineContext ExecuteIfNeeded(CliSubsystem subsystem, PipelineContext pipelineContext) + internal static PipelineResult ExecuteIfNeeded(CliSubsystem subsystem, PipelineResult pipelineContext) => subsystem.ExecuteIfNeeded(pipelineContext); } diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index a071dd550c..67dfb7ce7c 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -29,7 +29,7 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) /// /// Note to inheritors: Call base for all ValueSubsystem methods that you override to ensure correct behavior /// - protected internal override CliExit Execute(PipelineContext pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineContext) { parseResult ??= pipelineContext.ParseResult; return base.Execute(pipelineContext); diff --git a/src/System.CommandLine.Subsystems/VersionSubsystem.cs b/src/System.CommandLine.Subsystems/VersionSubsystem.cs index ee763fa55a..e7554cf7b6 100644 --- a/src/System.CommandLine.Subsystems/VersionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/VersionSubsystem.cs @@ -49,7 +49,7 @@ protected internal override CliConfiguration Initialize(InitializationContext co protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.GetValue("--version"); - protected internal override CliExit Execute(PipelineContext pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineContext) { var subsystemVersion = SpecificVersion; var version = subsystemVersion is null From 5f31f7c448832c2290c178ca7d214523980ae57a Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Thu, 23 May 2024 08:42:14 -0400 Subject: [PATCH 088/150] Renamed PipelineContext type and parameter names --- .../AlternateSubsystems.cs | 20 +++++++++---------- src/System.CommandLine.Subsystems/CliExit.cs | 4 ++-- .../CompletionSubsystem.cs | 6 +++--- .../Directives/DiagramSubsystem.cs | 10 +++++----- .../ErrorReportingSubsystem.cs | 10 +++++----- .../HelpSubsystem.cs | 6 +++--- src/System.CommandLine.Subsystems/Pipeline.cs | 20 +++++++++---------- .../Subsystems/CliSubsystem.cs | 16 +++++++-------- .../Subsystems/Subsystem.cs | 12 +++++------ .../ValueSubsystem.cs | 6 +++--- .../VersionSubsystem.cs | 8 ++++---- 11 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index f035c87297..d4db61f884 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -10,11 +10,11 @@ internal class AlternateSubsystems { internal class AlternateVersion : VersionSubsystem { - protected override CliExit Execute(PipelineResult pipelineContext) + protected override CliExit Execute(PipelineResult pipelineResult) { - pipelineContext.ConsoleHack.WriteLine($"***{CliExecutable.ExecutableVersion}***"); - pipelineContext.AlreadyHandled = true; - return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + pipelineResult.ConsoleHack.WriteLine($"***{CliExecutable.ExecutableVersion}***"); + pipelineResult.AlreadyHandled = true; + return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); } } @@ -29,12 +29,12 @@ public VersionThatUsesHelpData(CliSymbol symbol) private CliSymbol Symbol { get; } - protected override CliExit Execute(PipelineResult pipelineContext) + protected override CliExit Execute(PipelineResult pipelineResult) { TryGetAnnotation(Symbol, HelpAnnotations.Description, out string? description); - pipelineContext.ConsoleHack.WriteLine(description); - pipelineContext.AlreadyHandled = true; - return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + pipelineResult.ConsoleHack.WriteLine(description); + pipelineResult.AlreadyHandled = true; + return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); } } @@ -51,10 +51,10 @@ protected override CliConfiguration Initialize(InitializationContext context) return base.Initialize(context); } - protected override CliExit Execute(PipelineResult pipelineContext) + protected override CliExit Execute(PipelineResult pipelineResult) { ExecutionWasRun = true; - return base.Execute(pipelineContext); + return base.Execute(pipelineResult); } protected override CliExit TearDown(CliExit cliExit) diff --git a/src/System.CommandLine.Subsystems/CliExit.cs b/src/System.CommandLine.Subsystems/CliExit.cs index 460461d2cf..be1190675f 100644 --- a/src/System.CommandLine.Subsystems/CliExit.cs +++ b/src/System.CommandLine.Subsystems/CliExit.cs @@ -8,8 +8,8 @@ namespace System.CommandLine; // TODO: Consider what info is needed after invocation. If it's the whole pipeline context, consider collapsing this with that class. public class CliExit { - internal CliExit(PipelineResult pipelineContext) - : this(pipelineContext.ParseResult, pipelineContext.AlreadyHandled, pipelineContext.ExitCode) + internal CliExit(PipelineResult pipelineResult) + : this(pipelineResult.ParseResult, pipelineResult.AlreadyHandled, pipelineResult.ExitCode) { } private CliExit(ParseResult? parseResult, bool handled, int exitCode) diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index 11177b873d..3ec5b166da 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -18,9 +18,9 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) ? false : false; - protected internal override CliExit Execute(PipelineResult pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineResult) { - pipelineContext.ConsoleHack.WriteLine("Not yet implemented"); - return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + pipelineResult.ConsoleHack.WriteLine("Not yet implemented"); + return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); } } diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 242e07ffe6..3c0bdd1d49 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -13,14 +13,14 @@ public class DiagramSubsystem( IAnnotationProvider? annotationProvider = null) //protected internal override bool GetIsActivated(ParseResult? parseResult) // => parseResult is not null && option is not null && parseResult.GetValue(option); - protected internal override CliExit Execute(PipelineResult pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineResult) { // Gather locations - //var locations = pipelineContext.ParseResult.LocationMap - // .Concat(Map(pipelineContext.ParseResult.Configuration.PreProcessedLocations)); + //var locations = pipelineResult.ParseResult.LocationMap + // .Concat(Map(pipelineResult.ParseResult.Configuration.PreProcessedLocations)); - pipelineContext.ConsoleHack.WriteLine("Output diagram"); - return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + pipelineResult.ConsoleHack.WriteLine("Output diagram"); + return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); } diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs index ac5f1cb00b..0948ac0176 100644 --- a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -23,14 +23,14 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.Errors.Any(); // TODO: properly test execute directly when parse result is usable in tests - protected internal override CliExit Execute(PipelineContext pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineResult) { - var _ = pipelineContext.ParseResult - ?? throw new ArgumentException("The parse result has not been set", nameof(pipelineContext)); + var _ = pipelineResult.ParseResult + ?? throw new ArgumentException("The parse result has not been set", nameof(pipelineResult)); - Report(pipelineContext.ConsoleHack, pipelineContext.ParseResult.Errors); + Report(pipelineResult.ConsoleHack, pipelineResult.ParseResult.Errors); - return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); } public void Report(ConsoleHack consoleHack, IReadOnlyList errors) diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index 22d5a4b61d..3be155a18b 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -34,11 +34,11 @@ protected internal override CliConfiguration Initialize(InitializationContext co protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.GetValue(HelpOption); - protected internal override CliExit Execute(PipelineResult pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineResult) { // TODO: Match testable output pattern - pipelineContext.ConsoleHack.WriteLine("Help me!"); - return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + pipelineResult.ConsoleHack.WriteLine("Help me!"); + return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); } public bool TryGetDescription (CliSymbol symbol, out string? description) diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 7972e3b25c..18298cea7b 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -62,9 +62,9 @@ public CliExit Execute(CliConfiguration configuration, string[] args, string raw public CliExit Execute(ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) { - var pipelineContext = new PipelineResult(parseResult, rawInput, this, consoleHack ?? new ConsoleHack()); - ExecuteSubsystems(pipelineContext); - return new CliExit(pipelineContext); + var pipelineResult = new PipelineResult(parseResult, rawInput, this, consoleHack ?? new ConsoleHack()); + ExecuteSubsystems(pipelineResult); + return new CliExit(pipelineResult); } // TODO: Consider whether this should be public. It would simplify testing, but would it do anything else @@ -94,7 +94,7 @@ protected virtual void InitializeSubsystems(InitializationContext context) /// /// Perform any cleanup operations /// - /// The context of the current execution + /// The context of the current execution protected virtual CliExit TearDownSubsystems(CliExit cliExit) { // TODO: Work on this design as the last cliExit wins and they may not all be well behaved @@ -109,24 +109,24 @@ protected virtual CliExit TearDownSubsystems(CliExit cliExit) return cliExit; } - protected virtual void ExecuteSubsystems(PipelineResult pipelineContext) + protected virtual void ExecuteSubsystems(PipelineResult pipelineResult) { - // TODO: Consider redesign where pipelineContext is not modifiable. + // TODO: Consider redesign where pipelineResult is not modifiable. // foreach (var subsystem in Subsystems) { if (subsystem is not null) { - pipelineContext = subsystem.ExecuteIfNeeded(pipelineContext); + pipelineResult = subsystem.ExecuteIfNeeded(pipelineResult); } } } - protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineResult pipelineContext) + protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineResult pipelineResult) { - if (subsystem is not null && (!pipelineContext.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) + if (subsystem is not null && (!pipelineResult.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) { - subsystem.ExecuteIfNeeded(pipelineContext); + subsystem.ExecuteIfNeeded(pipelineResult); } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index d92c2396af..fdc22d5aae 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -65,21 +65,21 @@ protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId< /// /// Executes the behavior of the subsystem. For example, help would write information to the console. /// - /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate + /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate /// A CliExit object with information such as whether the CLI should terminate - protected internal virtual CliExit Execute(PipelineResult pipelineContext) - => CliExit.NotRun(pipelineContext.ParseResult); + protected internal virtual CliExit Execute(PipelineResult pipelineResult) + => CliExit.NotRun(pipelineResult.ParseResult); - internal PipelineResult ExecuteIfNeeded(PipelineResult pipelineContext) - => ExecuteIfNeeded(pipelineContext.ParseResult, pipelineContext); + internal PipelineResult ExecuteIfNeeded(PipelineResult pipelineResult) + => ExecuteIfNeeded(pipelineResult.ParseResult, pipelineResult); - internal PipelineResult ExecuteIfNeeded(ParseResult? parseResult, PipelineResult pipelineContext) + internal PipelineResult ExecuteIfNeeded(ParseResult? parseResult, PipelineResult pipelineResult) { if (GetIsActivated(parseResult)) { - Execute(pipelineContext); + Execute(pipelineResult); } - return pipelineContext; + return pipelineResult; } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs index ca99631395..440c313ef3 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs @@ -8,8 +8,8 @@ public class Subsystem public static void Initialize(CliSubsystem subsystem, CliConfiguration configuration, IReadOnlyList args) => subsystem.Initialize(new InitializationContext(configuration, args)); - public static CliExit Execute(CliSubsystem subsystem, PipelineResult pipelineContext) - => subsystem.Execute(pipelineContext); + public static CliExit Execute(CliSubsystem subsystem, PipelineResult pipelineResult) + => subsystem.Execute(pipelineResult); public static bool GetIsActivated(CliSubsystem subsystem, ParseResult parseResult) => subsystem.GetIsActivated(parseResult); @@ -21,9 +21,9 @@ public static CliExit Execute(CliSubsystem subsystem, ParseResult parseResult, s => subsystem.Execute(new PipelineResult(parseResult, rawInput, null, consoleHack)); - internal static PipelineResult ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack, PipelineResult? pipelineContext = null) - => subsystem.ExecuteIfNeeded(pipelineContext ?? new PipelineResult(parseResult, rawInput, null, consoleHack)); + internal static PipelineResult ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack, PipelineResult? pipelineResult = null) + => subsystem.ExecuteIfNeeded(pipelineResult ?? new PipelineResult(parseResult, rawInput, null, consoleHack)); - internal static PipelineResult ExecuteIfNeeded(CliSubsystem subsystem, PipelineResult pipelineContext) - => subsystem.ExecuteIfNeeded(pipelineContext); + internal static PipelineResult ExecuteIfNeeded(CliSubsystem subsystem, PipelineResult pipelineResult) + => subsystem.ExecuteIfNeeded(pipelineResult); } diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index 67dfb7ce7c..6152f019af 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -29,10 +29,10 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) /// /// Note to inheritors: Call base for all ValueSubsystem methods that you override to ensure correct behavior /// - protected internal override CliExit Execute(PipelineResult pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineResult) { - parseResult ??= pipelineContext.ParseResult; - return base.Execute(pipelineContext); + parseResult ??= pipelineResult.ParseResult; + return base.Execute(pipelineResult); } private void SetValue(CliSymbol symbol, object? value) diff --git a/src/System.CommandLine.Subsystems/VersionSubsystem.cs b/src/System.CommandLine.Subsystems/VersionSubsystem.cs index e7554cf7b6..c538a896c4 100644 --- a/src/System.CommandLine.Subsystems/VersionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/VersionSubsystem.cs @@ -49,15 +49,15 @@ protected internal override CliConfiguration Initialize(InitializationContext co protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.GetValue("--version"); - protected internal override CliExit Execute(PipelineResult pipelineContext) + protected internal override CliExit Execute(PipelineResult pipelineResult) { var subsystemVersion = SpecificVersion; var version = subsystemVersion is null ? CliExecutable.ExecutableVersion : subsystemVersion; - pipelineContext.ConsoleHack.WriteLine(version); - pipelineContext.AlreadyHandled = true; - return CliExit.SuccessfullyHandled(pipelineContext.ParseResult); + pipelineResult.ConsoleHack.WriteLine(version); + pipelineResult.AlreadyHandled = true; + return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); } } From 4443107eff529ffebe06ddbb0eb11e28782e5888 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Thu, 23 May 2024 09:47:55 -0400 Subject: [PATCH 089/150] Remove CliExit and add SetSuccess/NotRun The SetSuccess and NotRun methods are intended to abstract what those conditions mean from the actual properties that are set, probably making them private after discussio. I have more confidence in SetSuccess than NotRun --- .../AlternateSubsystems.cs | 17 ++++----- .../PipelineTests.cs | 10 +++--- .../VersionFunctionalTests.cs | 2 +- src/System.CommandLine.Subsystems/CliExit.cs | 35 ------------------- .../CompletionSubsystem.cs | 5 +-- .../Directives/DiagramSubsystem.cs | 5 +-- .../ErrorReportingSubsystem.cs | 5 +-- .../HelpSubsystem.cs | 5 +-- src/System.CommandLine.Subsystems/Pipeline.cs | 20 +++++------ .../Subsystems/CliSubsystem.cs | 13 ++++--- .../Subsystems/PipelineResult.cs | 12 +++++++ .../Subsystems/Subsystem.cs | 8 ++--- .../ValueSubsystem.cs | 2 +- .../VersionSubsystem.cs | 6 ++-- 14 files changed, 65 insertions(+), 80 deletions(-) delete mode 100644 src/System.CommandLine.Subsystems/CliExit.cs diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index d4db61f884..70ab7b2a24 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -10,11 +10,11 @@ internal class AlternateSubsystems { internal class AlternateVersion : VersionSubsystem { - protected override CliExit Execute(PipelineResult pipelineResult) + protected override PipelineResult Execute(PipelineResult pipelineResult) { pipelineResult.ConsoleHack.WriteLine($"***{CliExecutable.ExecutableVersion}***"); - pipelineResult.AlreadyHandled = true; - return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); + pipelineResult.SetSuccess(); + return pipelineResult; } } @@ -29,12 +29,13 @@ public VersionThatUsesHelpData(CliSymbol symbol) private CliSymbol Symbol { get; } - protected override CliExit Execute(PipelineResult pipelineResult) + protected override PipelineResult Execute(PipelineResult pipelineResult) { TryGetAnnotation(Symbol, HelpAnnotations.Description, out string? description); pipelineResult.ConsoleHack.WriteLine(description); pipelineResult.AlreadyHandled = true; - return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); + pipelineResult.SetSuccess(); + return pipelineResult; } } @@ -51,16 +52,16 @@ protected override CliConfiguration Initialize(InitializationContext context) return base.Initialize(context); } - protected override CliExit Execute(PipelineResult pipelineResult) + protected override PipelineResult Execute(PipelineResult pipelineResult) { ExecutionWasRun = true; return base.Execute(pipelineResult); } - protected override CliExit TearDown(CliExit cliExit) + protected override PipelineResult TearDown(PipelineResult pipelineResult) { TeardownWasRun = true; - return base.TearDown(cliExit); + return base.TearDown(pipelineResult); } } diff --git a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs index 27308a20e4..62f1ac8348 100644 --- a/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/PipelineTests.cs @@ -43,7 +43,7 @@ public void Subsystem_runs_in_pipeline_only_when_requested(string input, bool sh var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); + exit.AlreadyHandled.Should().Be(shouldRun); if (shouldRun) { console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); @@ -61,7 +61,7 @@ public void Subsystem_runs_with_explicit_parse_only_when_requested(string input, var exit = pipeline.Execute(result, input, console); exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); + exit.AlreadyHandled.Should().Be(shouldRun); if (shouldRun) { console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); @@ -79,7 +79,7 @@ public void Subsystem_runs_initialize_and_teardown_when_requested(string input, var exit = pipeline.Execute(GetNewTestConfiguration(), input, console); exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); + exit.AlreadyHandled.Should().Be(shouldRun); versionSubsystem.InitializationWasRun.Should().BeTrue(); versionSubsystem.ExecutionWasRun.Should().Be(shouldRun); versionSubsystem.TeardownWasRun.Should().BeTrue(); @@ -109,7 +109,7 @@ public void Subsystem_works_without_pipeline(string input, bool shouldRun) var exit = Subsystem.Execute(versionSubsystem, parseResult, input, console); exit.Should().NotBeNull(); exit.ExitCode.Should().Be(0); - exit.Handled.Should().BeTrue(); + exit.AlreadyHandled.Should().BeTrue(); console.GetBuffer().Trim().Should().Be(TestData.AssemblyVersionString); } } @@ -132,7 +132,7 @@ public void Subsystem_works_without_pipeline_style2(string input, bool shouldRun var exit = Subsystem.ExecuteIfNeeded(versionSubsystem, parseResult, input, console); exit.ExitCode.Should().Be(0); - exit.Handled.Should().Be(shouldRun); + exit.AlreadyHandled.Should().Be(shouldRun); console.GetBuffer().Trim().Should().Be(expectedVersion); } diff --git a/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs b/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs index dfb5681a53..8804cca62e 100644 --- a/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/VersionFunctionalTests.cs @@ -26,7 +26,7 @@ public void When_the_version_option_is_specified_then_the_version_is_written_to_ var exit = pipeline.Execute(configuration, "-v", consoleHack); exit.ExitCode.Should().Be(0); - exit.Handled.Should().BeTrue(); + exit.AlreadyHandled.Should().BeTrue(); consoleHack.GetBuffer().Should().Be($"{version}{newLine}"); } diff --git a/src/System.CommandLine.Subsystems/CliExit.cs b/src/System.CommandLine.Subsystems/CliExit.cs deleted file mode 100644 index be1190675f..0000000000 --- a/src/System.CommandLine.Subsystems/CliExit.cs +++ /dev/null @@ -1,35 +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.Subsystems; - -namespace System.CommandLine; - - // TODO: Consider what info is needed after invocation. If it's the whole pipeline context, consider collapsing this with that class. -public class CliExit -{ - internal CliExit(PipelineResult pipelineResult) - : this(pipelineResult.ParseResult, pipelineResult.AlreadyHandled, pipelineResult.ExitCode) - { } - - private CliExit(ParseResult? parseResult, bool handled, int exitCode) - { - ExitCode = exitCode; - Handled = handled; - ParseResult = parseResult; - } - public ParseResult? ParseResult { get; set; } - - public int ExitCode { get; } - - public static implicit operator int(CliExit cliExit) => cliExit.ExitCode; - - public static implicit operator bool(CliExit cliExit) => !cliExit.Handled; - - - public bool Handled { get; } - - public static CliExit NotRun(ParseResult? parseResult) => new(parseResult, false, 0); - - public static CliExit SuccessfullyHandled(ParseResult? parseResult) => new(parseResult, true, 0); -} diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index 3ec5b166da..47ed128053 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -18,9 +18,10 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) ? false : false; - protected internal override CliExit Execute(PipelineResult pipelineResult) + protected internal override PipelineResult Execute(PipelineResult pipelineResult) { pipelineResult.ConsoleHack.WriteLine("Not yet implemented"); - return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); + pipelineResult.SetSuccess(); + return pipelineResult; } } diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 3c0bdd1d49..e9a19ce2b0 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -13,14 +13,15 @@ public class DiagramSubsystem( IAnnotationProvider? annotationProvider = null) //protected internal override bool GetIsActivated(ParseResult? parseResult) // => parseResult is not null && option is not null && parseResult.GetValue(option); - protected internal override CliExit Execute(PipelineResult pipelineResult) + protected internal override PipelineResult Execute(PipelineResult pipelineResult) { // Gather locations //var locations = pipelineResult.ParseResult.LocationMap // .Concat(Map(pipelineResult.ParseResult.Configuration.PreProcessedLocations)); pipelineResult.ConsoleHack.WriteLine("Output diagram"); - return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); + pipelineResult.SetSuccess(); + return pipelineResult; } diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs index 0948ac0176..a7c0f81d24 100644 --- a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -23,14 +23,15 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.Errors.Any(); // TODO: properly test execute directly when parse result is usable in tests - protected internal override CliExit Execute(PipelineResult pipelineResult) + protected internal override PipelineResult Execute(PipelineResult pipelineResult) { var _ = pipelineResult.ParseResult ?? throw new ArgumentException("The parse result has not been set", nameof(pipelineResult)); Report(pipelineResult.ConsoleHack, pipelineResult.ParseResult.Errors); - return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); + pipelineResult.SetSuccess(); + return pipelineResult; } public void Report(ConsoleHack consoleHack, IReadOnlyList errors) diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index 3be155a18b..2156f59b7b 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -34,11 +34,12 @@ protected internal override CliConfiguration Initialize(InitializationContext co protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.GetValue(HelpOption); - protected internal override CliExit Execute(PipelineResult pipelineResult) + protected internal override PipelineResult Execute(PipelineResult pipelineResult) { // TODO: Match testable output pattern pipelineResult.ConsoleHack.WriteLine("Help me!"); - return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); + pipelineResult.SetSuccess(); + return pipelineResult; } public bool TryGetDescription (CliSymbol symbol, out string? description) diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 18298cea7b..cc6bedccea 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -51,20 +51,20 @@ public ParseResult Parse(CliConfiguration configuration, IReadOnlyList a return parseResult; } - public CliExit Execute(CliConfiguration configuration, string rawInput, ConsoleHack? consoleHack = null) + public PipelineResult Execute(CliConfiguration configuration, string rawInput, ConsoleHack? consoleHack = null) => Execute(configuration, CliParser.SplitCommandLine(rawInput).ToArray(), rawInput, consoleHack); - public CliExit Execute(CliConfiguration configuration, string[] args, string rawInput, ConsoleHack? consoleHack = null) + public PipelineResult Execute(CliConfiguration configuration, string[] args, string rawInput, ConsoleHack? consoleHack = null) { - var cliExit = Execute(Parse(configuration, args), rawInput, consoleHack); - return TearDownSubsystems(cliExit); + var pipelineResult = Execute(Parse(configuration, args), rawInput, consoleHack); + return TearDownSubsystems(pipelineResult); } - public CliExit Execute(ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) + public PipelineResult Execute(ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) { var pipelineResult = new PipelineResult(parseResult, rawInput, this, consoleHack ?? new ConsoleHack()); ExecuteSubsystems(pipelineResult); - return new CliExit(pipelineResult); + return pipelineResult; } // TODO: Consider whether this should be public. It would simplify testing, but would it do anything else @@ -95,18 +95,18 @@ protected virtual void InitializeSubsystems(InitializationContext context) /// Perform any cleanup operations /// /// The context of the current execution - protected virtual CliExit TearDownSubsystems(CliExit cliExit) + protected virtual PipelineResult TearDownSubsystems(PipelineResult pipelineResult) { - // TODO: Work on this design as the last cliExit wins and they may not all be well behaved + // TODO: Work on this design as the last pipelineResult wins and they may not all be well behaved var subsystems = Subsystems.Reverse(); foreach (var subsystem in subsystems) { if (subsystem is not null) { - cliExit = subsystem.TearDown(cliExit); + pipelineResult = subsystem.TearDown(pipelineResult); } } - return cliExit; + return pipelineResult; } protected virtual void ExecuteSubsystems(PipelineResult pipelineResult) diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index fdc22d5aae..99a821515e 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -66,9 +66,12 @@ protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId< /// Executes the behavior of the subsystem. For example, help would write information to the console. /// /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate - /// A CliExit object with information such as whether the CLI should terminate - protected internal virtual CliExit Execute(PipelineResult pipelineResult) - => CliExit.NotRun(pipelineResult.ParseResult); + /// A PipelineResult object with information such as whether the CLI should terminate + protected internal virtual PipelineResult Execute(PipelineResult pipelineResult) + { + pipelineResult.NotRun(pipelineResult.ParseResult); + return pipelineResult; + } internal PipelineResult ExecuteIfNeeded(PipelineResult pipelineResult) => ExecuteIfNeeded(pipelineResult.ParseResult, pipelineResult); @@ -111,7 +114,7 @@ protected internal virtual CliConfiguration Initialize(InitializationContext con => context.Configuration; // TODO: Determine if this is needed. - protected internal virtual CliExit TearDown(CliExit cliExit) - => cliExit; + protected internal virtual PipelineResult TearDown(PipelineResult pipelineResult) + => pipelineResult; } diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelineResult.cs b/src/System.CommandLine.Subsystems/Subsystems/PipelineResult.cs index 00c02bc9ac..2c9e56d73a 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/PipelineResult.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/PipelineResult.cs @@ -1,6 +1,8 @@ // 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.Subsystems; public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) @@ -13,4 +15,14 @@ public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? public bool AlreadyHandled { get; set; } public int ExitCode { get; set; } + public void NotRun(ParseResult? parseResult) + { + // no op because defaults are false and 0 + } + + public void SetSuccess() + { + AlreadyHandled = true; + ExitCode = 0; + } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs index 440c313ef3..c641982d7e 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs @@ -8,16 +8,16 @@ public class Subsystem public static void Initialize(CliSubsystem subsystem, CliConfiguration configuration, IReadOnlyList args) => subsystem.Initialize(new InitializationContext(configuration, args)); - public static CliExit Execute(CliSubsystem subsystem, PipelineResult pipelineResult) + public static PipelineResult Execute(CliSubsystem subsystem, PipelineResult pipelineResult) => subsystem.Execute(pipelineResult); public static bool GetIsActivated(CliSubsystem subsystem, ParseResult parseResult) => subsystem.GetIsActivated(parseResult); - public static CliExit ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) - => new(subsystem.ExecuteIfNeeded(new PipelineResult(parseResult, rawInput, null, consoleHack))); + public static PipelineResult ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) + => subsystem.ExecuteIfNeeded(new PipelineResult(parseResult, rawInput, null, consoleHack)); - public static CliExit Execute(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) + public static PipelineResult Execute(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) => subsystem.Execute(new PipelineResult(parseResult, rawInput, null, consoleHack)); diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index 6152f019af..92339ea673 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -29,7 +29,7 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) /// /// Note to inheritors: Call base for all ValueSubsystem methods that you override to ensure correct behavior /// - protected internal override CliExit Execute(PipelineResult pipelineResult) + protected internal override PipelineResult Execute(PipelineResult pipelineResult) { parseResult ??= pipelineResult.ParseResult; return base.Execute(pipelineResult); diff --git a/src/System.CommandLine.Subsystems/VersionSubsystem.cs b/src/System.CommandLine.Subsystems/VersionSubsystem.cs index c538a896c4..7a06003203 100644 --- a/src/System.CommandLine.Subsystems/VersionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/VersionSubsystem.cs @@ -49,15 +49,15 @@ protected internal override CliConfiguration Initialize(InitializationContext co protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.GetValue("--version"); - protected internal override CliExit Execute(PipelineResult pipelineResult) + protected internal override PipelineResult Execute(PipelineResult pipelineResult) { var subsystemVersion = SpecificVersion; var version = subsystemVersion is null ? CliExecutable.ExecutableVersion : subsystemVersion; pipelineResult.ConsoleHack.WriteLine(version); - pipelineResult.AlreadyHandled = true; - return CliExit.SuccessfullyHandled(pipelineResult.ParseResult); + pipelineResult.SetSuccess(); + return pipelineResult; } } From 7013b470f684ef3c264d454bc7ccd8cc09fafbc4 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Thu, 23 May 2024 13:27:39 -0400 Subject: [PATCH 090/150] Fluent style obscures that there is a single `PipelineResult` that is mutated. --- .../AlternateSubsystems.cs | 18 ++++++++---------- .../CompletionSubsystem.cs | 3 +-- .../Directives/DiagramSubsystem.cs | 3 +-- .../Directives/DirectiveSubsystem.cs | 4 +--- .../Directives/ResponseSubsystem.cs | 7 ++----- .../ErrorReportingSubsystem.cs | 3 +-- .../HelpSubsystem.cs | 11 +++-------- src/System.CommandLine.Subsystems/Pipeline.cs | 10 +++++----- .../Subsystems/CliSubsystem.cs | 15 ++++++--------- .../Subsystems/Subsystem.cs | 17 ++++++++++++----- .../ValueSubsystem.cs | 4 ++-- .../VersionSubsystem.cs | 7 ++----- 12 files changed, 44 insertions(+), 58 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index 70ab7b2a24..abab37351b 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -10,11 +10,10 @@ internal class AlternateSubsystems { internal class AlternateVersion : VersionSubsystem { - protected override PipelineResult Execute(PipelineResult pipelineResult) + protected override void Execute(PipelineResult pipelineResult) { pipelineResult.ConsoleHack.WriteLine($"***{CliExecutable.ExecutableVersion}***"); pipelineResult.SetSuccess(); - return pipelineResult; } } @@ -29,13 +28,12 @@ public VersionThatUsesHelpData(CliSymbol symbol) private CliSymbol Symbol { get; } - protected override PipelineResult Execute(PipelineResult pipelineResult) + protected override void Execute(PipelineResult pipelineResult) { TryGetAnnotation(Symbol, HelpAnnotations.Description, out string? description); pipelineResult.ConsoleHack.WriteLine(description); pipelineResult.AlreadyHandled = true; pipelineResult.SetSuccess(); - return pipelineResult; } } @@ -45,23 +43,23 @@ internal class VersionWithInitializeAndTeardown : VersionSubsystem internal bool ExecutionWasRun; internal bool TeardownWasRun; - protected override CliConfiguration Initialize(InitializationContext context) + protected override void Initialize(InitializationContext context) { + base.Initialize(context); // marker hack needed because ConsoleHack not available in initialization InitializationWasRun = true; - return base.Initialize(context); } - protected override PipelineResult Execute(PipelineResult pipelineResult) + protected override void Execute(PipelineResult pipelineResult) { ExecutionWasRun = true; - return base.Execute(pipelineResult); + base.Execute(pipelineResult); } - protected override PipelineResult TearDown(PipelineResult pipelineResult) + protected override void TearDown(PipelineResult pipelineResult) { TeardownWasRun = true; - return base.TearDown(pipelineResult); + base.TearDown(pipelineResult); } } diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index 47ed128053..434d4e1934 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -18,10 +18,9 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) ? false : false; - protected internal override PipelineResult Execute(PipelineResult pipelineResult) + protected internal override void Execute(PipelineResult pipelineResult) { pipelineResult.ConsoleHack.WriteLine("Not yet implemented"); pipelineResult.SetSuccess(); - return pipelineResult; } } diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index e9a19ce2b0..c31ea6e2a9 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -13,7 +13,7 @@ public class DiagramSubsystem( IAnnotationProvider? annotationProvider = null) //protected internal override bool GetIsActivated(ParseResult? parseResult) // => parseResult is not null && option is not null && parseResult.GetValue(option); - protected internal override PipelineResult Execute(PipelineResult pipelineResult) + protected internal override void Execute(PipelineResult pipelineResult) { // Gather locations //var locations = pipelineResult.ParseResult.LocationMap @@ -21,7 +21,6 @@ protected internal override PipelineResult Execute(PipelineResult pipelineResult pipelineResult.ConsoleHack.WriteLine("Output diagram"); pipelineResult.SetSuccess(); - return pipelineResult; } diff --git a/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs index 6b4b7d9670..efb51bceee 100644 --- a/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs @@ -19,7 +19,7 @@ public DirectiveSubsystem(string name, SubsystemKind kind, IAnnotationProvider? Id = id ?? name; } - protected internal override CliConfiguration Initialize(InitializationContext context) + protected internal override void Initialize(InitializationContext context) { for (int i = 0; i < context.Args.Count; i++) { @@ -50,8 +50,6 @@ protected internal override CliConfiguration Initialize(InitializationContext co break; } } - - return context.Configuration; } protected internal override bool GetIsActivated(ParseResult? parseResult) diff --git a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs index ed43c8d626..66a1fd6b79 100644 --- a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs @@ -9,11 +9,8 @@ namespace System.CommandLine.Directives; public class ResponseSubsystem() : CliSubsystem("Response", SubsystemKind.Response, null) { - protected internal override CliConfiguration Initialize(InitializationContext context) - { - context.Configuration.ResponseFileTokenReplacer = Replacer; - return context.Configuration; - } + protected internal override void Initialize(InitializationContext context) + => context.Configuration.ResponseFileTokenReplacer = Replacer; public static (List? tokens, List? errors) Replacer(string responseSourceName) { diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs index a7c0f81d24..9effb620f0 100644 --- a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -23,7 +23,7 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.Errors.Any(); // TODO: properly test execute directly when parse result is usable in tests - protected internal override PipelineResult Execute(PipelineResult pipelineResult) + protected internal override void Execute(PipelineResult pipelineResult) { var _ = pipelineResult.ParseResult ?? throw new ArgumentException("The parse result has not been set", nameof(pipelineResult)); @@ -31,7 +31,6 @@ protected internal override PipelineResult Execute(PipelineResult pipelineResult Report(pipelineResult.ConsoleHack, pipelineResult.ParseResult.Errors); pipelineResult.SetSuccess(); - return pipelineResult; } public void Report(ConsoleHack consoleHack, IReadOnlyList errors) diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index 2156f59b7b..b4a922420b 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -24,22 +24,17 @@ public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) Arity = ArgumentArity.Zero }; - protected internal override CliConfiguration Initialize(InitializationContext context) - { - context.Configuration.RootCommand.Add(HelpOption); - - return context.Configuration; - } + protected internal override void Initialize(InitializationContext context) + => context.Configuration.RootCommand.Add(HelpOption); protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.GetValue(HelpOption); - protected internal override PipelineResult Execute(PipelineResult pipelineResult) + protected internal override void Execute(PipelineResult pipelineResult) { // TODO: Match testable output pattern pipelineResult.ConsoleHack.WriteLine("Help me!"); pipelineResult.SetSuccess(); - return pipelineResult; } public bool TryGetDescription (CliSymbol symbol, out string? description) diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index cc6bedccea..4bceb1db18 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -57,7 +57,8 @@ public PipelineResult Execute(CliConfiguration configuration, string rawInput, C public PipelineResult Execute(CliConfiguration configuration, string[] args, string rawInput, ConsoleHack? consoleHack = null) { var pipelineResult = Execute(Parse(configuration, args), rawInput, consoleHack); - return TearDownSubsystems(pipelineResult); + TearDownSubsystems(pipelineResult); + return pipelineResult; } public PipelineResult Execute(ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) @@ -95,7 +96,7 @@ protected virtual void InitializeSubsystems(InitializationContext context) /// Perform any cleanup operations /// /// The context of the current execution - protected virtual PipelineResult TearDownSubsystems(PipelineResult pipelineResult) + protected virtual void TearDownSubsystems(PipelineResult pipelineResult) { // TODO: Work on this design as the last pipelineResult wins and they may not all be well behaved var subsystems = Subsystems.Reverse(); @@ -103,10 +104,9 @@ protected virtual PipelineResult TearDownSubsystems(PipelineResult pipelineResul { if (subsystem is not null) { - pipelineResult = subsystem.TearDown(pipelineResult); + subsystem.TearDown(pipelineResult); } } - return pipelineResult; } protected virtual void ExecuteSubsystems(PipelineResult pipelineResult) @@ -117,7 +117,7 @@ protected virtual void ExecuteSubsystems(PipelineResult pipelineResult) { if (subsystem is not null) { - pipelineResult = subsystem.ExecuteIfNeeded(pipelineResult); + subsystem.ExecuteIfNeeded(pipelineResult); } } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 99a821515e..157a45f441 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -67,11 +67,8 @@ protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId< /// /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate /// A PipelineResult object with information such as whether the CLI should terminate - protected internal virtual PipelineResult Execute(PipelineResult pipelineResult) - { - pipelineResult.NotRun(pipelineResult.ParseResult); - return pipelineResult; - } + protected internal virtual void Execute(PipelineResult pipelineResult) + => pipelineResult.NotRun(pipelineResult.ParseResult); internal PipelineResult ExecuteIfNeeded(PipelineResult pipelineResult) => ExecuteIfNeeded(pipelineResult.ParseResult, pipelineResult); @@ -110,11 +107,11 @@ internal PipelineResult ExecuteIfNeeded(ParseResult? parseResult, PipelineResult /// True if parsing should continue // there might be a better design that supports a message // TODO: Because of this and similar usage, consider combining CLI declaration and config. ArgParse calls this the parser, which I like // TODO: Why does Intitialize return a configuration? - protected internal virtual CliConfiguration Initialize(InitializationContext context) - => context.Configuration; + protected internal virtual void Initialize(InitializationContext context) + {} // TODO: Determine if this is needed. - protected internal virtual PipelineResult TearDown(PipelineResult pipelineResult) - => pipelineResult; + protected internal virtual void TearDown(PipelineResult pipelineResult) + {} } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs index c641982d7e..e55d28a7d7 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Subsystem.cs @@ -8,7 +8,7 @@ public class Subsystem public static void Initialize(CliSubsystem subsystem, CliConfiguration configuration, IReadOnlyList args) => subsystem.Initialize(new InitializationContext(configuration, args)); - public static PipelineResult Execute(CliSubsystem subsystem, PipelineResult pipelineResult) + public static void Execute(CliSubsystem subsystem, PipelineResult pipelineResult) => subsystem.Execute(pipelineResult); public static bool GetIsActivated(CliSubsystem subsystem, ParseResult parseResult) @@ -18,12 +18,19 @@ public static PipelineResult ExecuteIfNeeded(CliSubsystem subsystem, ParseResult => subsystem.ExecuteIfNeeded(new PipelineResult(parseResult, rawInput, null, consoleHack)); public static PipelineResult Execute(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) - => subsystem.Execute(new PipelineResult(parseResult, rawInput, null, consoleHack)); - + { + var pipelineResult = new PipelineResult(parseResult, rawInput,null, consoleHack); + subsystem.Execute(pipelineResult); + return pipelineResult; + } internal static PipelineResult ExecuteIfNeeded(CliSubsystem subsystem, ParseResult parseResult, string rawInput, ConsoleHack? consoleHack, PipelineResult? pipelineResult = null) - => subsystem.ExecuteIfNeeded(pipelineResult ?? new PipelineResult(parseResult, rawInput, null, consoleHack)); + { + pipelineResult ??= new PipelineResult(parseResult, rawInput, null, consoleHack); + subsystem.ExecuteIfNeeded(pipelineResult ); + return pipelineResult; + } - internal static PipelineResult ExecuteIfNeeded(CliSubsystem subsystem, PipelineResult pipelineResult) + internal static void ExecuteIfNeeded(CliSubsystem subsystem, PipelineResult pipelineResult) => subsystem.ExecuteIfNeeded(pipelineResult); } diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index 92339ea673..2257743ccd 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -29,10 +29,10 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) /// /// Note to inheritors: Call base for all ValueSubsystem methods that you override to ensure correct behavior /// - protected internal override PipelineResult Execute(PipelineResult pipelineResult) + protected internal override void Execute(PipelineResult pipelineResult) { parseResult ??= pipelineResult.ParseResult; - return base.Execute(pipelineResult); + base.Execute(pipelineResult); } private void SetValue(CliSymbol symbol, object? value) diff --git a/src/System.CommandLine.Subsystems/VersionSubsystem.cs b/src/System.CommandLine.Subsystems/VersionSubsystem.cs index 7a06003203..8cb07d116d 100644 --- a/src/System.CommandLine.Subsystems/VersionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/VersionSubsystem.cs @@ -34,22 +34,20 @@ public string? SpecificVersion ?.GetCustomAttribute() ?.InformationalVersion; - protected internal override CliConfiguration Initialize(InitializationContext context) + protected internal override void Initialize(InitializationContext context) { var option = new CliOption("--version", ["-v"]) { Arity = ArgumentArity.Zero }; context.Configuration.RootCommand.Add(option); - - return context.Configuration; } // TODO: Stash option rather than using string protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.GetValue("--version"); - protected internal override PipelineResult Execute(PipelineResult pipelineResult) + protected internal override void Execute(PipelineResult pipelineResult) { var subsystemVersion = SpecificVersion; var version = subsystemVersion is null @@ -57,7 +55,6 @@ protected internal override PipelineResult Execute(PipelineResult pipelineResult : subsystemVersion; pipelineResult.ConsoleHack.WriteLine(version); pipelineResult.SetSuccess(); - return pipelineResult; } } From 096365de1763e54d595daf4254c4d1d2e1a6182b Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Wed, 17 Jul 2024 12:34:50 -0400 Subject: [PATCH 091/150] Updated TFM --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index b619acaa4f..8d6c3c92bb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,7 @@ - net7.0 + net8 From 8b8eeb5d3a5f8662ff962c7240b82c2c0a6690e4 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Tue, 23 Jul 2024 10:35:51 -0700 Subject: [PATCH 092/150] Update Directory.Build.props --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 8d6c3c92bb..41b9b11ba1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,7 @@ - net8 + net8.0 From 1064c506a954e96694fe3cfad435f35e16cc00ef Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Thu, 23 May 2024 09:47:55 -0400 Subject: [PATCH 093/150] Remove spurious using statement --- src/System.CommandLine.Subsystems/CompletionSubsystem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index 434d4e1934..7d418d0c59 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -3,6 +3,7 @@ using System.CommandLine.Subsystems; using System.CommandLine.Subsystems.Annotations; +using System.Reflection.Metadata.Ecma335; namespace System.CommandLine; From 9e001fef5d0e499e33a7dae786ccab72a27c1083 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sat, 25 May 2024 05:41:46 -0400 Subject: [PATCH 094/150] Added PipelinePhase --- .../CompletionSubsystem.cs | 2 +- .../Directives/DiagramSubsystem.cs | 176 +++++++++--------- .../Directives/DirectiveSubsystem.cs | 2 +- .../Directives/ResponseSubsystem.cs | 2 +- .../HelpSubsystem.cs | 6 +- .../Pipeline.Subsystems.cs | 79 ++++++++ src/System.CommandLine.Subsystems/Pipeline.cs | 75 +++++--- .../{Subsystems => }/PipelineResult.cs | 4 +- .../Subsystems/CliSubsystem.cs | 12 +- .../Subsystems/PhaseTiming.cs | 10 + .../Subsystems/PipelinePhase.cs | 172 +++++++++++++++++ .../Subsystems/SubsystemKind.cs | 1 + .../ValueSubsystem.cs | 2 +- .../VersionSubsystem.cs | 2 +- 14 files changed, 414 insertions(+), 131 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/Pipeline.Subsystems.cs rename src/System.CommandLine.Subsystems/{Subsystems => }/PipelineResult.cs (95%) create mode 100644 src/System.CommandLine.Subsystems/Subsystems/PhaseTiming.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index 7d418d0c59..75777ec2f2 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -10,7 +10,7 @@ namespace System.CommandLine; public class CompletionSubsystem : CliSubsystem { public CompletionSubsystem(IAnnotationProvider? annotationProvider = null) - : base(CompletionAnnotations.Prefix, SubsystemKind.Completion, annotationProvider) + : base(CompletionAnnotations.Prefix, SubsystemKind.Completion, annotationProvider) { } // TODO: Figure out trigger for completions diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index c31ea6e2a9..2381ed6e49 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -7,7 +7,7 @@ namespace System.CommandLine.Directives; -public class DiagramSubsystem( IAnnotationProvider? annotationProvider = null) +public class DiagramSubsystem(IAnnotationProvider? annotationProvider = null) : DirectiveSubsystem("diagram", SubsystemKind.Diagram, annotationProvider) { //protected internal override bool GetIsActivated(ParseResult? parseResult) @@ -68,115 +68,115 @@ private static void Diagram( builder.Append('!'); } */ -// TODO: Directives -/* - switch (symbolResult) + // TODO: Directives + /* + switch (symbolResult) + { + case DirectiveResult { Directive: not DiagramDirective }: + break; + */ + + // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives + /* + case ArgumentResult argumentResult: { - case DirectiveResult { Directive: not DiagramDirective }: - break; - */ + var includeArgumentName = + argumentResult.Argument.FirstParent!.Symbol is CliCommand { HasArguments: true, Arguments.Count: > 1 }; - // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives - /* - case ArgumentResult argumentResult: + if (includeArgumentName) + { + builder.Append("[ "); + builder.Append(argumentResult.Argument.Name); + builder.Append(' '); + } + + if (argumentResult.Argument.Arity.MaximumNumberOfValues > 0) + { + ArgumentConversionResult conversionResult = argumentResult.GetArgumentConversionResult(); + switch (conversionResult.Result) { - var includeArgumentName = - argumentResult.Argument.FirstParent!.Symbol is CliCommand { HasArguments: true, Arguments.Count: > 1 }; - - if (includeArgumentName) - { - builder.Append("[ "); - builder.Append(argumentResult.Argument.Name); - builder.Append(' '); - } - - if (argumentResult.Argument.Arity.MaximumNumberOfValues > 0) - { - ArgumentConversionResult conversionResult = argumentResult.GetArgumentConversionResult(); - switch (conversionResult.Result) + case ArgumentConversionResultType.NoArgument: + break; + case ArgumentConversionResultType.Successful: + switch (conversionResult.Value) { - case ArgumentConversionResultType.NoArgument: + case string s: + builder.Append($"<{s}>"); break; - case ArgumentConversionResultType.Successful: - switch (conversionResult.Value) - { - case string s: - builder.Append($"<{s}>"); - break; - - case IEnumerable items: - builder.Append('<'); - builder.Append( - string.Join("> <", - items.Cast().ToArray())); - builder.Append('>'); - break; - - default: - builder.Append('<'); - builder.Append(conversionResult.Value); - builder.Append('>'); - break; - } + case IEnumerable items: + builder.Append('<'); + builder.Append( + string.Join("> <", + items.Cast().ToArray())); + builder.Append('>'); break; - default: // failures + default: builder.Append('<'); - builder.Append(string.Join("> <", symbolResult.Tokens.Select(t => t.Value))); + builder.Append(conversionResult.Value); builder.Append('>'); - break; } - } - if (includeArgumentName) - { - builder.Append(" ]"); - } + break; - break; + default: // failures + builder.Append('<'); + builder.Append(string.Join("> <", symbolResult.Tokens.Select(t => t.Value))); + builder.Append('>'); + + break; } + } - default: - { - OptionResult? optionResult = symbolResult as OptionResult; - - if (optionResult is { Implicit: true }) - { - builder.Append('*'); - } - - builder.Append("[ "); - - if (optionResult is not null) - { - builder.Append(optionResult.IdentifierToken?.Value ?? optionResult.Option.Name); - } - else - { - builder.Append(((CommandResult)symbolResult).IdentifierToken.Value); - } - - foreach (SymbolResult child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) - { - if (child is ArgumentResult arg && - (arg.Argument.ValueType == typeof(bool) || - arg.Argument.Arity.MaximumNumberOfValues == 0)) - { - continue; - } + if (includeArgumentName) + { + builder.Append(" ]"); + } - builder.Append(' '); + break; + } - Diagram(builder, child, parseResult); - } + default: + { + OptionResult? optionResult = symbolResult as OptionResult; - builder.Append(" ]"); - break; + if (optionResult is { Implicit: true }) + { + builder.Append('*'); + } + + builder.Append("[ "); + + if (optionResult is not null) + { + builder.Append(optionResult.IdentifierToken?.Value ?? optionResult.Option.Name); + } + else + { + builder.Append(((CommandResult)symbolResult).IdentifierToken.Value); + } + + foreach (SymbolResult child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) + { + if (child is ArgumentResult arg && + (arg.Argument.ValueType == typeof(bool) || + arg.Argument.Arity.MaximumNumberOfValues == 0)) + { + continue; } + + builder.Append(' '); + + Diagram(builder, child, parseResult); } + + builder.Append(" ]"); + break; } } +} +} */ } diff --git a/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs index efb51bceee..8616d71491 100644 --- a/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs @@ -13,7 +13,7 @@ public abstract class DirectiveSubsystem : CliSubsystem public string Id { get; } public Location? Location { get; private set; } - public DirectiveSubsystem(string name, SubsystemKind kind, IAnnotationProvider? annotationProvider = null, string? id = null) + public DirectiveSubsystem(string name, SubsystemKind kind, IAnnotationProvider? annotationProvider = null, string? id = null) : base(name, kind, annotationProvider: annotationProvider) { Id = id ?? name; diff --git a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs index 66a1fd6b79..bb30e118d5 100644 --- a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs @@ -9,7 +9,7 @@ namespace System.CommandLine.Directives; public class ResponseSubsystem() : CliSubsystem("Response", SubsystemKind.Response, null) { - protected internal override void Initialize(InitializationContext context) + protected internal override void Initialize(InitializationContext context) => context.Configuration.ResponseFileTokenReplacer = Replacer; public static (List? tokens, List? errors) Replacer(string responseSourceName) diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index b4a922420b..67af643177 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -24,7 +24,7 @@ public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) Arity = ArgumentArity.Zero }; - protected internal override void Initialize(InitializationContext context) + protected internal override void Initialize(InitializationContext context) => context.Configuration.RootCommand.Add(HelpOption); protected internal override bool GetIsActivated(ParseResult? parseResult) @@ -37,6 +37,6 @@ protected internal override void Execute(PipelineResult pipelineResult) pipelineResult.SetSuccess(); } - public bool TryGetDescription (CliSymbol symbol, out string? description) - => TryGetAnnotation (symbol, HelpAnnotations.Description, out description); + public bool TryGetDescription(CliSymbol symbol, out string? description) + => TryGetAnnotation(symbol, HelpAnnotations.Description, out description); } diff --git a/src/System.CommandLine.Subsystems/Pipeline.Subsystems.cs b/src/System.CommandLine.Subsystems/Pipeline.Subsystems.cs new file mode 100644 index 0000000000..682525f9a9 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Pipeline.Subsystems.cs @@ -0,0 +1,79 @@ +// 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; +using System.CommandLine.Subsystems; + +namespace System.CommandLine; + +public partial class Pipeline +{ + private class Subsystems : IEnumerable + { + internal List subsystemList = []; + private bool dirty; + + internal void Add(CliSubsystem? subsystem, bool insertAtStart = false) + { + if (subsystem is not null) + { + // TODO: Determine whether to remove and readd. This affects the position in the list + //if (subsystemList.Contains(subsystem)) + //{ + // subsystemList.Remove(subsystem); + //} + subsystemList.Add(subsystem); + dirty = true; + } + } + + internal void Insert(CliSubsystem? subsystem, CliSubsystem existingSubsystem, bool insertBefore = false) + { + if (subsystem is not null) + { + if (existingSubsystem.Phase != subsystem.Phase) + { + throw new InvalidOperationException("Subsystems can only be inserted relative to other subsystems in the same phase"); + } + if (subsystemList.Contains(subsystem)) + { + // TODO: Exception or last wins? Same for Add above + throw new InvalidOperationException("Subsystems can only be inserted if it had not already been added"); + } + + var existing = subsystemList.IndexOf(existingSubsystem); + if (existing != -1) + { + throw new InvalidOperationException("Subsystems can only be added relative to subsystems that have previously been added"); + } + + var insertAt = insertBefore ? existing + 1 : existing; + subsystemList.Insert(insertAt, subsystem); + dirty = true; + } + } + + public IEnumerator GetEnumerator() + { + return subsystemList.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return subsystemList.GetEnumerator(); + } + + internal IEnumerable EarlyReturnSubsystems + => subsystemList.Where(x => x.Phase == SubsystemPhase.EarlyReturn).ToList(); + + internal IEnumerable ValidationSubsystems + => subsystemList.Where(x => x.Phase == SubsystemPhase.Validate).ToList(); + + internal IEnumerable ExecutionSubsystems + => subsystemList.Where(x => x.Phase == SubsystemPhase.Execute).ToList(); + + internal IEnumerable FinishSubsystems + => subsystemList.Where(x => x.Phase == SubsystemPhase.Finish).ToList(); + + } +} diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 4bceb1db18..389230f77f 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -7,11 +7,10 @@ namespace System.CommandLine; -public class Pipeline +public partial class Pipeline { //TODO: When we allow adding subsystems, this code will change - private IEnumerable Subsystems - => [Help, Version, Completion, Diagram, Value, ErrorReporting]; + private readonly Subsystems subsystems = new(); public static Pipeline Create(HelpSubsystem? help = null, VersionSubsystem? version = null, @@ -19,7 +18,8 @@ public static Pipeline Create(HelpSubsystem? help = null, DiagramSubsystem? diagram = null, ErrorReportingSubsystem? errorReporting = null, ValueSubsystem? value = null) - => new() + { + Pipeline pipeline = new() { Help = help ?? new HelpSubsystem(), Version = version ?? new VersionSubsystem(), @@ -28,12 +28,46 @@ public static Pipeline Create(HelpSubsystem? help = null, ErrorReporting = errorReporting ?? new ErrorReportingSubsystem(), Value = value ?? new ValueSubsystem() }; + // This order is based on: if the user entered both, which should they get? + // * It is reasonable to diagram help and completion. More reasonable than getting help on Diagram or Completion + // * A future version of Help and Version may take arguments/options. In that case, help on version is reasonable. + pipeline.AddSubsystem(pipeline.Diagram); + pipeline.AddSubsystem(pipeline.Completion); + pipeline.AddSubsystem(pipeline.Help); + pipeline.AddSubsystem(pipeline.Version); + //pipeline.AddSubsystem(pipeline.Value); + pipeline.AddSubsystem(pipeline.ErrorReporting); + + return pipeline; + } public static Pipeline CreateEmpty() => new(); private Pipeline() { } + public void AddSubsystem(CliSubsystem? subsystem, bool insertAtStart = false) + => subsystems.Add(subsystem, insertAtStart); + + public void InsertSubsystemAfter(CliSubsystem? subsystem, CliSubsystem existingSubsystem) + => subsystems.Insert(subsystem, existingSubsystem); + + public void InsertSubsystemBefore(CliSubsystem? subsystem, CliSubsystem existingSubsystem) + => subsystems.Insert(subsystem, existingSubsystem, true); + + public IEnumerable EarlyReturnSubsystems + => subsystems.EarlyReturnSubsystems; + + public IEnumerable ValidationSubsystems + => subsystems.ValidationSubsystems; + + public IEnumerable ExecutionSubsystems + => subsystems.ExecutionSubsystems; + + public IEnumerable FinishSubsystems + => subsystems.FinishSubsystems; + + public HelpSubsystem? Help { get; set; } public VersionSubsystem? Version { get; set; } public CompletionSubsystem? Completion { get; set; } @@ -55,16 +89,16 @@ public PipelineResult Execute(CliConfiguration configuration, string rawInput, C => Execute(configuration, CliParser.SplitCommandLine(rawInput).ToArray(), rawInput, consoleHack); public PipelineResult Execute(CliConfiguration configuration, string[] args, string rawInput, ConsoleHack? consoleHack = null) - { - var pipelineResult = Execute(Parse(configuration, args), rawInput, consoleHack); - TearDownSubsystems(pipelineResult); - return pipelineResult; - } + => Execute(Parse(configuration, args), rawInput, consoleHack); public PipelineResult Execute(ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) { var pipelineResult = new PipelineResult(parseResult, rawInput, this, consoleHack ?? new ConsoleHack()); - ExecuteSubsystems(pipelineResult); + ExecuteSubsystems(EarlyReturnSubsystems, pipelineResult); + ExecuteSubsystems(ValidationSubsystems, pipelineResult); + ExecuteSubsystems(ExecutionSubsystems, pipelineResult); + ExecuteSubsystems(FinishSubsystems, pipelineResult); + TearDownSubsystems(pipelineResult); return pipelineResult; } @@ -81,7 +115,7 @@ public PipelineResult Execute(ParseResult parseResult, string rawInput, ConsoleH /// protected virtual void InitializeSubsystems(InitializationContext context) { - foreach (var subsystem in Subsystems) + foreach (var subsystem in subsystems) { if (subsystem is not null) { @@ -99,7 +133,7 @@ protected virtual void InitializeSubsystems(InitializationContext context) protected virtual void TearDownSubsystems(PipelineResult pipelineResult) { // TODO: Work on this design as the last pipelineResult wins and they may not all be well behaved - var subsystems = Subsystems.Reverse(); + var subsystems = this.subsystems.Reverse(); foreach (var subsystem in subsystems) { if (subsystem is not null) @@ -109,25 +143,14 @@ protected virtual void TearDownSubsystems(PipelineResult pipelineResult) } } - protected virtual void ExecuteSubsystems(PipelineResult pipelineResult) + private static void ExecuteSubsystems(IEnumerable subsystems, PipelineResult pipelineResult) { - // TODO: Consider redesign where pipelineResult is not modifiable. - // - foreach (var subsystem in Subsystems) + foreach (var subsystem in subsystems) { - if (subsystem is not null) + if (subsystem is not null && (!pipelineResult.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) { subsystem.ExecuteIfNeeded(pipelineResult); } } } - - protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineResult pipelineResult) - { - if (subsystem is not null && (!pipelineResult.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) - { - subsystem.ExecuteIfNeeded(pipelineResult); - } - } - } diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelineResult.cs b/src/System.CommandLine.Subsystems/PipelineResult.cs similarity index 95% rename from src/System.CommandLine.Subsystems/Subsystems/PipelineResult.cs rename to src/System.CommandLine.Subsystems/PipelineResult.cs index 2c9e56d73a..1b236fb9ec 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/PipelineResult.cs +++ b/src/System.CommandLine.Subsystems/PipelineResult.cs @@ -1,9 +1,7 @@ // 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.Subsystems; +namespace System.CommandLine; public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) { diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 157a45f441..f4c785ae96 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -12,11 +12,11 @@ namespace System.CommandLine.Subsystems; /// public abstract class CliSubsystem { - protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProvider? annotationProvider) + protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProvider? annotationProvider) { Name = name; _annotationProvider = annotationProvider; - SubsystemKind = subsystemKind; + Kind = subsystemKind; } /// @@ -27,7 +27,7 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv /// /// Defines the kind of subsystem, such as help or version /// - public SubsystemKind SubsystemKind { get; } + public SubsystemKind Kind { get; } private readonly IAnnotationProvider? _annotationProvider; @@ -67,7 +67,7 @@ protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId< /// /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate /// A PipelineResult object with information such as whether the CLI should terminate - protected internal virtual void Execute(PipelineResult pipelineResult) + protected internal virtual void Execute(PipelineResult pipelineResult) => pipelineResult.NotRun(pipelineResult.ParseResult); internal PipelineResult ExecuteIfNeeded(PipelineResult pipelineResult) @@ -108,10 +108,10 @@ internal PipelineResult ExecuteIfNeeded(ParseResult? parseResult, PipelineResult // TODO: Because of this and similar usage, consider combining CLI declaration and config. ArgParse calls this the parser, which I like // TODO: Why does Intitialize return a configuration? protected internal virtual void Initialize(InitializationContext context) - {} + { } // TODO: Determine if this is needed. protected internal virtual void TearDown(PipelineResult pipelineResult) - {} + { } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/PhaseTiming.cs b/src/System.CommandLine.Subsystems/Subsystems/PhaseTiming.cs new file mode 100644 index 0000000000..ea07f16f85 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/PhaseTiming.cs @@ -0,0 +1,10 @@ +// 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.Subsystems; + +public enum PhaseTiming +{ + Before = 0, + After +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs b/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs new file mode 100644 index 0000000000..4c25f04f8f --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs @@ -0,0 +1,172 @@ +// 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.Subsystems; + +/// +/// This struct manages one phase. The most common case is that it is empty, and the most complicated +/// case of several items before, and several items after will be quite rare. +/// +/// +/// The most common case is that it is empty, and the most complicated +/// case of several items before, and several items after will be quite rare.
+///
+internal struct PipelinePhase +{ + private List? before = null; + private List? after = null; + + public PipelinePhase(CliSubsystem subsystem) + { + Subsystem = subsystem; + } + + public readonly SubsystemKind Kind + => Subsystem.Kind; + + internal CliSubsystem Subsystem { get; set; } + + public void AddSubsystem(CliSubsystem subsystem, PhaseTiming timing = PhaseTiming.Before) + { + List? addToList = timing == PhaseTiming.Before + ? CreateBeforeIfNeeded() + : CreateAfterIfNeeded(); + + addToList.Add(subsystem); + } + + private List CreateBeforeIfNeeded() + { + before ??= []; + return before; + } + + private List CreateAfterIfNeeded() + { + after ??= []; + return after; + } + + public IEnumerable GetSubsystems() + { + List ret = Subsystem is null + ? [] + : [Subsystem]; + if (before is not null) + { + // TODO: Confirm that we want to reverse the before list. + ret.AddRange(((IEnumerable)before).Reverse()); + } + if (after is not null) + { + ret.AddRange(after); + } + return ret; + } +} + + + +// AddSubsystem(CliSubsystem subsystem, SubsystemPhase phase = SubsystemPhase.NotSpecified); + +//public enum SubsystemPhase +//{ +// NotSpecified = 0, +// BeforeDiagram, +// Diagram, +// AfterDiagram, +// BeforeCompletion, +// Completion, +// AfterCompletion, +// BeforeHelp, +// Help, +// AfterHelp, +// BeforeVersion, +// Version, +// AfterVersion, +// BeforeErrorReporting, +// ErrorReporting, +// AfterErrorReporting, +//} + +// AddSubsystem(CliSubsystem subsystem, SubsystemPhase phase = SubsystemPhase.NotSpecified, PhaseTiming timing = PhaseTiming.Before); + +//public enum SubsystemPhase +//{ +// NotSpecified = 0, +// Diagram, +// Completion, +// Help, +// Version, +// ErrorReporting, +//} + +//public enum PhaseTiming +//{ +// Before = 0, +// After +//} + + +///// +///// Subsystem phases group subsystems that should be run at specific places in CLI processing and +///// are used for high level ordering. +///// +///// +///// Order of operations: +///// +///// * Initialize is called for all subsystems, regardless of phase +///// * ExecuteIfNeeded is called for subsystems in the EarlyReturn phase +///// * ExecuteIfNeeded is called for subsystems in the Validate phase +///// * ExecuteIfNeeded is called for subsystems in the Execute phase +///// * ExecuteIfNeeded is called for subsystems in the Finish phase +///// * Teardown is called for all subsystems, regardless of phase +///// +//public enum SubsystemPhase +//{ +// /// +// /// Indicates a subsystem that never runs, and exists to support other subsystems. ValueSubsystem +// /// is an example. +// /// +// /// +// /// Initialization runs first, teardown runs last - this is arbitrary and can be changed prior +// /// to release if we have scenarios to justify. +// /// +// None, + +// /// +// /// Indicates a subsystem is designed to shortcut execution and perform an action other than the +// /// action indicated by the command. HelpSubsystem and VersionSubsystem are examples. +// /// +// /// +// /// EarlyReturn subsystems are differentiated from other execution because data validation has not +// /// occurred. Because of this, data should not be used and should be assume to be questionable. +// /// +// EarlyReturn, + +// /// +// /// Indicates a subsystem that validates data entered by the user. +// /// +// /// +// /// Errors are not reported, but are rather stored for later display. This may be reconsidered +// /// if we keep track of which errors have been reported. +// /// +// Validate, + +// /// +// /// Indicates a subsystem that executes using data entered by the user. The only known case is +// /// the Invocation subsystem. +// /// +// Execute, + +// /// +// /// Indicates a subsystem that runs as the CLI part of processing is ending. ErrorReportingSubsystem +// /// is an example, although we may rethink when errors are displayed. +// /// +// /// +// /// This is separate from the TearDown step, which is avaiable to all subsystems. +// /// +// Finish, +//} + + diff --git a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs index 8962392915..b6f36c8d11 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs @@ -14,3 +14,4 @@ public enum SubsystemKind Diagram, Response } + diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index 2257743ccd..0ff3abb255 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -75,7 +75,7 @@ not null when TryGetValue(symbol, out var value) //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) // => UseValue(symbol, GetEnvByName(envName)), not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValueCalculation, out var defaultValueCalculation) - => UseValue(symbol, CalculatedDefault(symbol, (Func) defaultValueCalculation)), + => UseValue(symbol, CalculatedDefault(symbol, (Func)defaultValueCalculation)), not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var explicitValue) => UseValue(symbol, (T)explicitValue), null => throw new ArgumentNullException(nameof(symbol)), diff --git a/src/System.CommandLine.Subsystems/VersionSubsystem.cs b/src/System.CommandLine.Subsystems/VersionSubsystem.cs index 8cb07d116d..7da5a655e4 100644 --- a/src/System.CommandLine.Subsystems/VersionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/VersionSubsystem.cs @@ -29,7 +29,7 @@ public string? SpecificVersion set => specificVersion = value; } - public static string? AssemblyVersion(Assembly assembly) + public static string? AssemblyVersion(Assembly assembly) => assembly ?.GetCustomAttribute() ?.InformationalVersion; From 01bf6488f5e9217f422cf6028f3253d35084b33f Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 26 May 2024 09:36:33 -0400 Subject: [PATCH 095/150] Added Enabled to ResponseSubsystem --- .../ResponseSubsystemTests.cs | 1 + .../Directives/ResponseSubsystem.cs | 35 +++++++++++-------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ResponseSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ResponseSubsystemTests.cs index 363a576e71..2e7bb8af1d 100644 --- a/src/System.CommandLine.Subsystems.Tests/ResponseSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ResponseSubsystemTests.cs @@ -19,6 +19,7 @@ public void Simple_response_file_contributes_to_parsing() var rootCommand = new CliRootCommand { option }; var configuration = new CliConfiguration(rootCommand); var subsystem = new ResponseSubsystem(); + subsystem.Enabled = true; string[] args = ["@Response_1.rsp"]; Subsystem.Initialize(subsystem, configuration, args); diff --git a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs index bb30e118d5..ebd31cf150 100644 --- a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs @@ -9,26 +9,33 @@ namespace System.CommandLine.Directives; public class ResponseSubsystem() : CliSubsystem("Response", SubsystemKind.Response, null) { + public bool Enabled { get; set; } + protected internal override void Initialize(InitializationContext context) => context.Configuration.ResponseFileTokenReplacer = Replacer; - public static (List? tokens, List? errors) Replacer(string responseSourceName) + public (List? tokens, List? errors) Replacer(string responseSourceName) { - try - { - // TODO: Include checks from previous system. - var contents = File.ReadAllText(responseSourceName); - return (CliParser.SplitCommandLine(contents).ToList(), null); - } - catch + if (Enabled) { - // TODO: Switch to proper errors - return (null, - errors: - [ - $"Failed to open response file {responseSourceName}" - ]); + try + { + // TODO: Include checks from previous system. + var contents = File.ReadAllText(responseSourceName); + return (CliParser.SplitCommandLine(contents).ToList(), null); + } + catch + { + // TODO: Switch to proper errors + return (null, + errors: + [ + $"Failed to open response file {responseSourceName}" + ]); + } } + // TODO: Confirm this is not an error state + return ([responseSourceName], null); } // TODO: File handling from previous system - ensure these checks are done (note: no tests caught these oversights From f0ead71eba1c5f2243a27e31522fc5fa6965104f Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 26 May 2024 09:38:38 -0400 Subject: [PATCH 096/150] Added new subsystems --- .../InvocationSubsystem.cs | 11 +++++++++++ .../Subsystems/Annotations/InvocationAnnotations.cs | 13 +++++++++++++ .../Subsystems/Annotations/ValidationAnnotations.cs | 13 +++++++++++++ .../Subsystems/SubsystemKind.cs | 10 ++++++---- .../ValidationSubsystem.cs | 11 +++++++++++ 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/InvocationSubsystem.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/InvocationAnnotations.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/ValidationAnnotations.cs create mode 100644 src/System.CommandLine.Subsystems/ValidationSubsystem.cs diff --git a/src/System.CommandLine.Subsystems/InvocationSubsystem.cs b/src/System.CommandLine.Subsystems/InvocationSubsystem.cs new file mode 100644 index 0000000000..a451065831 --- /dev/null +++ b/src/System.CommandLine.Subsystems/InvocationSubsystem.cs @@ -0,0 +1,11 @@ +// 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.Subsystems; +using System.CommandLine.Subsystems.Annotations; + +namespace System.CommandLine; + +public class InvocationSubsystem(IAnnotationProvider? annotationProvider = null) + : CliSubsystem(InvocationAnnotations.Prefix, SubsystemKind.Invocation, annotationProvider) +{} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/InvocationAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/InvocationAnnotations.cs new file mode 100644 index 0000000000..76756c3a46 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/InvocationAnnotations.cs @@ -0,0 +1,13 @@ +// 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.Subsystems.Annotations; + +/// +/// IDs for well-known Version annotations. +/// +public static class InvocationAnnotations +{ + internal static string Prefix { get; } = nameof(SubsystemKind.Invocation); + +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValidationAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValidationAnnotations.cs new file mode 100644 index 0000000000..78155e5358 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValidationAnnotations.cs @@ -0,0 +1,13 @@ +// 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.Subsystems.Annotations; + +/// +/// IDs for well-known Version annotations. +/// +public static class ValidationAnnotations +{ + internal static string Prefix { get; } = nameof(SubsystemKind.Validation); + +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs index b6f36c8d11..f565372de0 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/SubsystemKind.cs @@ -6,12 +6,14 @@ namespace System.CommandLine.Subsystems; public enum SubsystemKind { Other = 0, + Diagram, + Completion, Help, Version, - Value, + Validation, + Invocation, ErrorReporting, - Completion, - Diagram, - Response + Value, + Response, } diff --git a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs new file mode 100644 index 0000000000..29a8a5c5cd --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs @@ -0,0 +1,11 @@ +// 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.Subsystems; +using System.CommandLine.Subsystems.Annotations; + +namespace System.CommandLine; + +public class ValidationSubsystem(IAnnotationProvider? annotationProvider = null) + : CliSubsystem(ValidationAnnotations.Prefix, SubsystemKind.Validation, annotationProvider) +{ } From 336f568dd5b6576f343cd5d1235c080ee714f889 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 26 May 2024 09:43:13 -0400 Subject: [PATCH 097/150] Updated Pipeline to use PipelinePhases * Removed nested Pipelin.Subsystems * Removed tests due to immutable ValueSubsystem * Fixed bugs found in testing: * Kind needs to be passed to PipelinePhase constructor, Subsystem may be null so is not userful for retrieving * Phase list needs to be added in Pipeline constructor so is available to empty pipeline * PipelinePhrase needs to be a reference type as changes need to be available * Logic for getting specific subsystems didn't account for nulls correclty * Teardown now consistently runs Execute * Invocation and Validation are phases --- .../ValueSubsystemTests.cs | 3 +- .../Pipeline.Subsystems.cs | 79 ----- src/System.CommandLine.Subsystems/Pipeline.cs | 299 ++++++++++++++---- .../Subsystems/PipelinePhase.cs | 17 +- 4 files changed, 252 insertions(+), 146 deletions(-) delete mode 100644 src/System.CommandLine.Subsystems/Pipeline.Subsystems.cs diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index 1db461c0b1..797fb748fc 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -29,7 +29,7 @@ public void ValueSubsystem_is_activated_by_default() isActive.Should().BeTrue(); } - + /* Hold these tests until we determine if ValueSubsystem is replaceable [Fact] public void ValueSubsystem_returns_values_that_are_entered() { @@ -90,4 +90,5 @@ public void ValueSubsystem_returns_calculated_default_value_when_no_value_is_ent pipeline.Value.GetValue(option).Should().Be(expected); } + */ } diff --git a/src/System.CommandLine.Subsystems/Pipeline.Subsystems.cs b/src/System.CommandLine.Subsystems/Pipeline.Subsystems.cs deleted file mode 100644 index 682525f9a9..0000000000 --- a/src/System.CommandLine.Subsystems/Pipeline.Subsystems.cs +++ /dev/null @@ -1,79 +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; -using System.CommandLine.Subsystems; - -namespace System.CommandLine; - -public partial class Pipeline -{ - private class Subsystems : IEnumerable - { - internal List subsystemList = []; - private bool dirty; - - internal void Add(CliSubsystem? subsystem, bool insertAtStart = false) - { - if (subsystem is not null) - { - // TODO: Determine whether to remove and readd. This affects the position in the list - //if (subsystemList.Contains(subsystem)) - //{ - // subsystemList.Remove(subsystem); - //} - subsystemList.Add(subsystem); - dirty = true; - } - } - - internal void Insert(CliSubsystem? subsystem, CliSubsystem existingSubsystem, bool insertBefore = false) - { - if (subsystem is not null) - { - if (existingSubsystem.Phase != subsystem.Phase) - { - throw new InvalidOperationException("Subsystems can only be inserted relative to other subsystems in the same phase"); - } - if (subsystemList.Contains(subsystem)) - { - // TODO: Exception or last wins? Same for Add above - throw new InvalidOperationException("Subsystems can only be inserted if it had not already been added"); - } - - var existing = subsystemList.IndexOf(existingSubsystem); - if (existing != -1) - { - throw new InvalidOperationException("Subsystems can only be added relative to subsystems that have previously been added"); - } - - var insertAt = insertBefore ? existing + 1 : existing; - subsystemList.Insert(insertAt, subsystem); - dirty = true; - } - } - - public IEnumerator GetEnumerator() - { - return subsystemList.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return subsystemList.GetEnumerator(); - } - - internal IEnumerable EarlyReturnSubsystems - => subsystemList.Where(x => x.Phase == SubsystemPhase.EarlyReturn).ToList(); - - internal IEnumerable ValidationSubsystems - => subsystemList.Where(x => x.Phase == SubsystemPhase.Validate).ToList(); - - internal IEnumerable ExecutionSubsystems - => subsystemList.Where(x => x.Phase == SubsystemPhase.Execute).ToList(); - - internal IEnumerable FinishSubsystems - => subsystemList.Where(x => x.Phase == SubsystemPhase.Finish).ToList(); - - } -} diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 389230f77f..e3c55707a9 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -9,15 +9,34 @@ namespace System.CommandLine; public partial class Pipeline { - //TODO: When we allow adding subsystems, this code will change - private readonly Subsystems subsystems = new(); + // TODO: Consider more phases that have obvious meanings, like first and last + private PipelinePhase diagramPhase = new(SubsystemKind.Diagram); + private PipelinePhase completionPhase = new(SubsystemKind.Completion); + private PipelinePhase helpPhase = new(SubsystemKind.Help); + private PipelinePhase versionPhase = new(SubsystemKind.Version); + private PipelinePhase validationPhase = new(SubsystemKind.Validation); + private PipelinePhase invocationPhase = new(SubsystemKind.Invocation); + private PipelinePhase errorReportingPhase = new(SubsystemKind.ErrorReporting); + // TODO: Consider this naming as it sounds like it is a finishing phase + private readonly IEnumerable phases = []; + /// + /// Creates an instance of the pipeline using standard features. + /// + /// A help subsystem to replace the standard one. To add a subsystem, use AddSubsystem. + /// A help subsystem to replace the standard one. To add a subsystem, use AddSubsystem. + /// A help subsystem to replace the standard one. To add a subsystem, use AddSubsystem. + /// A help subsystem to replace the standard one. To add a subsystem, use AddSubsystem. + /// A help subsystem to replace the standard one. To add a subsystem, use AddSubsystem. + /// A new pipeline. + /// + /// Currently, the standard , , and cannot be replaced. is disabled by default. + /// public static Pipeline Create(HelpSubsystem? help = null, VersionSubsystem? version = null, CompletionSubsystem? completion = null, DiagramSubsystem? diagram = null, - ErrorReportingSubsystem? errorReporting = null, - ValueSubsystem? value = null) + ErrorReportingSubsystem? errorReporting = null) { Pipeline pipeline = new() { @@ -26,54 +45,223 @@ public static Pipeline Create(HelpSubsystem? help = null, Completion = completion ?? new CompletionSubsystem(), Diagram = diagram ?? new DiagramSubsystem(), ErrorReporting = errorReporting ?? new ErrorReportingSubsystem(), - Value = value ?? new ValueSubsystem() }; - // This order is based on: if the user entered both, which should they get? - // * It is reasonable to diagram help and completion. More reasonable than getting help on Diagram or Completion - // * A future version of Help and Version may take arguments/options. In that case, help on version is reasonable. - pipeline.AddSubsystem(pipeline.Diagram); - pipeline.AddSubsystem(pipeline.Completion); - pipeline.AddSubsystem(pipeline.Help); - pipeline.AddSubsystem(pipeline.Version); - //pipeline.AddSubsystem(pipeline.Value); - pipeline.AddSubsystem(pipeline.ErrorReporting); + return pipeline; } + /// + /// Creates an instance of the pipeline with no features. Use this if you want to explicitly add features. + /// + /// A new pipeline. + /// + /// The ValueSubsystem and is always added and cannot be changed. + /// public static Pipeline CreateEmpty() => new(); - private Pipeline() { } + private Pipeline() + { + Value = new ValueSubsystem(); + Response = new ResponseSubsystem(); + Invocation = new InvocationSubsystem(); + Validation = new ValidationSubsystem(); - public void AddSubsystem(CliSubsystem? subsystem, bool insertAtStart = false) - => subsystems.Add(subsystem, insertAtStart); + // This order is based on: if the user entered both, which should they get? + // * It is reasonable to diagram help and completion. More reasonable than getting help on Diagram or Completion + // * A future version of Help and Version may take arguments/options. In that case, help on version is reasonable. + phases = + [ + diagramPhase, completionPhase, helpPhase, versionPhase, + validationPhase, invocationPhase, errorReportingPhase + ]; + } + + /// + /// Enables response files. They are disabled by default. + /// + public bool ResponseFilesEnabled + { + get => Response.Enabled; + set => Response.Enabled = value; + } + + /// + /// Adds a subsystem. Use the property for the subsystem to replace the standard property. Use this method if you want an an additional subsystem. + /// + /// The subsystem to add. + /// indicates that the subsystem should run before all other subsystems in the phase, and indicates it should run after other subsystems. The default is . + /// + /// + /// The phase in which the subsystem runs is determined by the subsystem. + /// + public void AddSubsystem(CliSubsystem subsystem, PhaseTiming timing = PhaseTiming.Before) + { + switch (subsystem.Kind) + { + // TODO: Determine how Other should work. Do we have a kind and a phase? Perhaps just for Other subsystems. I think it is helpful to know it is something unforeseen + case SubsystemKind.Other: + break; + case SubsystemKind.Response: + case SubsystemKind.Value: + throw new InvalidOperationException($"You cannot add subsystems to {subsystem.Kind}"); + case SubsystemKind.Diagram: + diagramPhase.AddSubsystem(subsystem, timing); + break; + case SubsystemKind.Completion: + completionPhase.AddSubsystem(subsystem, timing); + break; + case SubsystemKind.Help: + helpPhase.AddSubsystem(subsystem, timing); + break; + case SubsystemKind.Version: + versionPhase.AddSubsystem(subsystem, timing); + break; + case SubsystemKind.ErrorReporting: + errorReportingPhase.AddSubsystem(subsystem, timing); + break; + } + } + + /// + /// Sets or gets the diagramming subsystem. + /// + public DiagramSubsystem? Diagram + { + get + => diagramPhase.Subsystem switch + { + null => null, + DiagramSubsystem diagramSubsystem => diagramSubsystem, + _ => throw new InvalidOperationException("Version subsystem is not of the correct type") + }; + set + { + diagramPhase.Subsystem = value; + } + } + + /// + /// Sets or gets the completion subsystem. + /// + public CompletionSubsystem? Completion + { + get + => completionPhase.Subsystem switch + { + null => null, + CompletionSubsystem completionSubsystem => completionSubsystem, + _ => throw new InvalidOperationException("Version subsystem is not of the correct type") + }; + set + { + completionPhase.Subsystem = value; + } + } + + /// + /// Sets or gets the help subsystem. + /// + public HelpSubsystem? Help + { + get + => helpPhase.Subsystem switch + { + null => null, + HelpSubsystem helpSubsystem => helpSubsystem, + _ => throw new InvalidOperationException("Version subsystem is not of the correct type") + }; + set + { + helpPhase.Subsystem = value; + } + } - public void InsertSubsystemAfter(CliSubsystem? subsystem, CliSubsystem existingSubsystem) - => subsystems.Insert(subsystem, existingSubsystem); + /// + /// Sets or gets the version subsystem. + /// + public VersionSubsystem? Version + { + get + => versionPhase.Subsystem switch + { + null => null, + VersionSubsystem versionSubsystem => versionSubsystem, + _ => throw new InvalidOperationException("Version subsystem is not of the correct type") + }; + set + { + versionPhase.Subsystem = value; + } + } - public void InsertSubsystemBefore(CliSubsystem? subsystem, CliSubsystem existingSubsystem) - => subsystems.Insert(subsystem, existingSubsystem, true); + /// + /// Sets or gets the error reporting subsystem. + /// + public ErrorReportingSubsystem? ErrorReporting + { + get + => errorReportingPhase.Subsystem switch + { + null => null, + ErrorReportingSubsystem errorReportingSubsystem => errorReportingSubsystem, + _ => throw new InvalidOperationException("Version subsystem is not of the correct type") + }; + set + { + errorReportingPhase.Subsystem = value; + } + } - public IEnumerable EarlyReturnSubsystems - => subsystems.EarlyReturnSubsystems; + /// + /// Sets or gets the validation subsystem + /// + public ValidationSubsystem? Validation + { + get + => validationPhase.Subsystem switch + { + null => null, + ValidationSubsystem validationSubsystem => validationSubsystem, + _ => throw new InvalidOperationException("Version subsystem is not of the correct type") + }; + set + { + validationPhase.Subsystem = value; + } + } - public IEnumerable ValidationSubsystems - => subsystems.ValidationSubsystems; - public IEnumerable ExecutionSubsystems - => subsystems.ExecutionSubsystems; + /// + /// Sets or gets the invocation subsystem + /// + public InvocationSubsystem? Invocation + { + get + => invocationPhase.Subsystem switch + { + null => null, + InvocationSubsystem invocationSubsystem => invocationSubsystem, + _ => throw new InvalidOperationException("Version subsystem is not of the correct type") + }; + set + { + invocationPhase.Subsystem = value; + } + } - public IEnumerable FinishSubsystems - => subsystems.FinishSubsystems; + // TODO: Are there use cases to replace this? Do we want new default values to require a new ValueSubsystem, which would block getting response providers from two sources. + /// + /// Sets or gets the value subsystem which manages entered and default values. + /// + public ValueSubsystem Value { get; } - public HelpSubsystem? Help { get; set; } - public VersionSubsystem? Version { get; set; } - public CompletionSubsystem? Completion { get; set; } - public DiagramSubsystem? Diagram { get; set; } - public ErrorReportingSubsystem? ErrorReporting { get; set; } - public ValueSubsystem? Value { get; set; } + /// + /// Sets or gets the response file subsystem + /// + public ResponseSubsystem Response { get; } public ParseResult Parse(CliConfiguration configuration, string rawInput) => Parse(configuration, CliParser.SplitCommandLine(rawInput).ToArray()); @@ -94,10 +282,17 @@ public PipelineResult Execute(CliConfiguration configuration, string[] args, str public PipelineResult Execute(ParseResult parseResult, string rawInput, ConsoleHack? consoleHack = null) { var pipelineResult = new PipelineResult(parseResult, rawInput, this, consoleHack ?? new ConsoleHack()); - ExecuteSubsystems(EarlyReturnSubsystems, pipelineResult); - ExecuteSubsystems(ValidationSubsystems, pipelineResult); - ExecuteSubsystems(ExecutionSubsystems, pipelineResult); - ExecuteSubsystems(FinishSubsystems, pipelineResult); + foreach (var phase in phases) + { + // TODO: Allow subsystems to control short-circuiting + foreach (var subsystem in phase.GetSubsystems()) + { + if (subsystem is not null && (!pipelineResult.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) + { + subsystem.ExecuteIfNeeded(pipelineResult); + } + } + } TearDownSubsystems(pipelineResult); return pipelineResult; } @@ -115,11 +310,12 @@ public PipelineResult Execute(ParseResult parseResult, string rawInput, ConsoleH /// protected virtual void InitializeSubsystems(InitializationContext context) { - foreach (var subsystem in subsystems) + foreach (var phase in phases) { - if (subsystem is not null) + // TODO: Allow subsystems to control short-circuiting? Not sure we need that for initialization + foreach (var subsystem in phase.GetSubsystems()) { - subsystem.Initialize(context); + subsystem?.Initialize(context); } } } @@ -133,23 +329,12 @@ protected virtual void InitializeSubsystems(InitializationContext context) protected virtual void TearDownSubsystems(PipelineResult pipelineResult) { // TODO: Work on this design as the last pipelineResult wins and they may not all be well behaved - var subsystems = this.subsystems.Reverse(); - foreach (var subsystem in subsystems) - { - if (subsystem is not null) - { - subsystem.TearDown(pipelineResult); - } - } - } - - private static void ExecuteSubsystems(IEnumerable subsystems, PipelineResult pipelineResult) - { - foreach (var subsystem in subsystems) + foreach (var phase in phases) { - if (subsystem is not null && (!pipelineResult.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) + // TODO: Allow subsystems to control short-circuiting? Not sure we need that for teardown + foreach (var subsystem in phase.GetSubsystems()) { - subsystem.ExecuteIfNeeded(pipelineResult); + subsystem?.TearDown(pipelineResult); } } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs b/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs index 4c25f04f8f..0570bafb83 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs @@ -8,23 +8,22 @@ namespace System.CommandLine.Subsystems; /// case of several items before, and several items after will be quite rare. /// /// +/// /// The most common case is that it is empty, and the most complicated /// case of several items before, and several items after will be quite rare.
+///
+/// +/// In the current design, this needs to be a reference type so values are synced. +/// ///
-internal struct PipelinePhase +internal class PipelinePhase(SubsystemKind kind) { private List? before = null; private List? after = null; - public PipelinePhase(CliSubsystem subsystem) - { - Subsystem = subsystem; - } - - public readonly SubsystemKind Kind - => Subsystem.Kind; + public readonly SubsystemKind Kind = kind; - internal CliSubsystem Subsystem { get; set; } + internal CliSubsystem? Subsystem { get; set; } public void AddSubsystem(CliSubsystem subsystem, PhaseTiming timing = PhaseTiming.Before) { From b883f9ffdcf018b35bbb36f08cc8ab1a94cf8543 Mon Sep 17 00:00:00 2001 From: KathleenDollard Date: Sun, 26 May 2024 15:43:49 -0400 Subject: [PATCH 098/150] Make PipelinePhase generic This avoids a type check and moves a runtime error to compiler time for setting the main phase subsystem, which must be of the correct type r null. Currently base and generic are in the same file to facilitate review (not having all the code show up as new in commits) --- src/System.CommandLine.Subsystems/Pipeline.cs | 107 +++--------- .../Subsystems/PipelinePhase.cs | 162 +++++------------- 2 files changed, 64 insertions(+), 205 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index e3c55707a9..598edd64b1 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -10,13 +10,13 @@ namespace System.CommandLine; public partial class Pipeline { // TODO: Consider more phases that have obvious meanings, like first and last - private PipelinePhase diagramPhase = new(SubsystemKind.Diagram); - private PipelinePhase completionPhase = new(SubsystemKind.Completion); - private PipelinePhase helpPhase = new(SubsystemKind.Help); - private PipelinePhase versionPhase = new(SubsystemKind.Version); - private PipelinePhase validationPhase = new(SubsystemKind.Validation); - private PipelinePhase invocationPhase = new(SubsystemKind.Invocation); - private PipelinePhase errorReportingPhase = new(SubsystemKind.ErrorReporting); + private PipelinePhase diagramPhase = new(SubsystemKind.Diagram); + private PipelinePhase completionPhase = new(SubsystemKind.Completion); + private PipelinePhase helpPhase = new(SubsystemKind.Help); + private PipelinePhase versionPhase = new(SubsystemKind.Version); + private PipelinePhase validationPhase = new(SubsystemKind.Validation); + private PipelinePhase invocationPhase = new(SubsystemKind.Invocation); + private PipelinePhase errorReportingPhase = new(SubsystemKind.ErrorReporting); // TODO: Consider this naming as it sounds like it is a finishing phase private readonly IEnumerable phases = []; @@ -129,17 +129,8 @@ public void AddSubsystem(CliSubsystem subsystem, PhaseTiming timing = PhaseTimin /// public DiagramSubsystem? Diagram { - get - => diagramPhase.Subsystem switch - { - null => null, - DiagramSubsystem diagramSubsystem => diagramSubsystem, - _ => throw new InvalidOperationException("Version subsystem is not of the correct type") - }; - set - { - diagramPhase.Subsystem = value; - } + get => diagramPhase.Subsystem; + set => diagramPhase.Subsystem = value; } /// @@ -147,17 +138,9 @@ public DiagramSubsystem? Diagram /// public CompletionSubsystem? Completion { - get - => completionPhase.Subsystem switch - { - null => null, - CompletionSubsystem completionSubsystem => completionSubsystem, - _ => throw new InvalidOperationException("Version subsystem is not of the correct type") - }; - set - { - completionPhase.Subsystem = value; - } + get => completionPhase.Subsystem; + set => completionPhase.Subsystem = value; + } /// @@ -165,17 +148,9 @@ public CompletionSubsystem? Completion /// public HelpSubsystem? Help { - get - => helpPhase.Subsystem switch - { - null => null, - HelpSubsystem helpSubsystem => helpSubsystem, - _ => throw new InvalidOperationException("Version subsystem is not of the correct type") - }; - set - { - helpPhase.Subsystem = value; - } + get => helpPhase.Subsystem; + set => helpPhase.Subsystem = value; + } /// @@ -183,17 +158,8 @@ public HelpSubsystem? Help /// public VersionSubsystem? Version { - get - => versionPhase.Subsystem switch - { - null => null, - VersionSubsystem versionSubsystem => versionSubsystem, - _ => throw new InvalidOperationException("Version subsystem is not of the correct type") - }; - set - { - versionPhase.Subsystem = value; - } + get => versionPhase.Subsystem; + set => versionPhase.Subsystem = value; } /// @@ -201,17 +167,8 @@ public VersionSubsystem? Version /// public ErrorReportingSubsystem? ErrorReporting { - get - => errorReportingPhase.Subsystem switch - { - null => null, - ErrorReportingSubsystem errorReportingSubsystem => errorReportingSubsystem, - _ => throw new InvalidOperationException("Version subsystem is not of the correct type") - }; - set - { - errorReportingPhase.Subsystem = value; - } + get => errorReportingPhase.Subsystem; + set => errorReportingPhase.Subsystem = value; } /// @@ -219,17 +176,8 @@ public ErrorReportingSubsystem? ErrorReporting /// public ValidationSubsystem? Validation { - get - => validationPhase.Subsystem switch - { - null => null, - ValidationSubsystem validationSubsystem => validationSubsystem, - _ => throw new InvalidOperationException("Version subsystem is not of the correct type") - }; - set - { - validationPhase.Subsystem = value; - } + get => validationPhase.Subsystem; + set => validationPhase.Subsystem = value; } @@ -238,17 +186,8 @@ public ValidationSubsystem? Validation /// public InvocationSubsystem? Invocation { - get - => invocationPhase.Subsystem switch - { - null => null, - InvocationSubsystem invocationSubsystem => invocationSubsystem, - _ => throw new InvalidOperationException("Version subsystem is not of the correct type") - }; - set - { - invocationPhase.Subsystem = value; - } + get => invocationPhase.Subsystem; + set => invocationPhase.Subsystem = value; } diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs b/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs index 0570bafb83..9187c3dca4 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs @@ -3,28 +3,22 @@ namespace System.CommandLine.Subsystems; -/// -/// This struct manages one phase. The most common case is that it is empty, and the most complicated -/// case of several items before, and several items after will be quite rare. -/// -/// -/// -/// The most common case is that it is empty, and the most complicated -/// case of several items before, and several items after will be quite rare.
-///
-/// -/// In the current design, this needs to be a reference type so values are synced. -/// -///
internal class PipelinePhase(SubsystemKind kind) { private List? before = null; private List? after = null; public readonly SubsystemKind Kind = kind; - - internal CliSubsystem? Subsystem { get; set; } - + protected CliSubsystem? CliSubsystem { get; set; } + + /// + /// Add a subsystem to the phase + /// + /// The subsystem to add + /// Whether it should run before or after the key subsystem + /// + /// Adding a subsystem that is not of the normal phase type is expected and OK. + /// public void AddSubsystem(CliSubsystem subsystem, PhaseTiming timing = PhaseTiming.Before) { List? addToList = timing == PhaseTiming.Before @@ -48,9 +42,9 @@ private List CreateAfterIfNeeded() public IEnumerable GetSubsystems() { - List ret = Subsystem is null + List ret = CliSubsystem is null ? [] - : [Subsystem]; + : [CliSubsystem]; if (before is not null) { // TODO: Confirm that we want to reverse the before list. @@ -64,108 +58,34 @@ public IEnumerable GetSubsystems() } } +/// +/// This struct manages one phase. The most common case is that it is empty, and the most complicated +/// case of several items before, and several items after will be quite rare. +/// +/// +/// +/// The most common case is that it is empty, and the most complicated +/// case of several items before, and several items after will be quite rare.
+///
+/// +/// In the current design, this needs to be a reference type so values are synced. +/// +///
+internal class PipelinePhase : PipelinePhase + where TSubsystem : CliSubsystem +{ + private TSubsystem? subsystem; + public PipelinePhase(SubsystemKind kind) : base(kind) + { } -// AddSubsystem(CliSubsystem subsystem, SubsystemPhase phase = SubsystemPhase.NotSpecified); - -//public enum SubsystemPhase -//{ -// NotSpecified = 0, -// BeforeDiagram, -// Diagram, -// AfterDiagram, -// BeforeCompletion, -// Completion, -// AfterCompletion, -// BeforeHelp, -// Help, -// AfterHelp, -// BeforeVersion, -// Version, -// AfterVersion, -// BeforeErrorReporting, -// ErrorReporting, -// AfterErrorReporting, -//} - -// AddSubsystem(CliSubsystem subsystem, SubsystemPhase phase = SubsystemPhase.NotSpecified, PhaseTiming timing = PhaseTiming.Before); - -//public enum SubsystemPhase -//{ -// NotSpecified = 0, -// Diagram, -// Completion, -// Help, -// Version, -// ErrorReporting, -//} - -//public enum PhaseTiming -//{ -// Before = 0, -// After -//} - - -///// -///// Subsystem phases group subsystems that should be run at specific places in CLI processing and -///// are used for high level ordering. -///// -///// -///// Order of operations: -///// -///// * Initialize is called for all subsystems, regardless of phase -///// * ExecuteIfNeeded is called for subsystems in the EarlyReturn phase -///// * ExecuteIfNeeded is called for subsystems in the Validate phase -///// * ExecuteIfNeeded is called for subsystems in the Execute phase -///// * ExecuteIfNeeded is called for subsystems in the Finish phase -///// * Teardown is called for all subsystems, regardless of phase -///// -//public enum SubsystemPhase -//{ -// /// -// /// Indicates a subsystem that never runs, and exists to support other subsystems. ValueSubsystem -// /// is an example. -// /// -// /// -// /// Initialization runs first, teardown runs last - this is arbitrary and can be changed prior -// /// to release if we have scenarios to justify. -// /// -// None, - -// /// -// /// Indicates a subsystem is designed to shortcut execution and perform an action other than the -// /// action indicated by the command. HelpSubsystem and VersionSubsystem are examples. -// /// -// /// -// /// EarlyReturn subsystems are differentiated from other execution because data validation has not -// /// occurred. Because of this, data should not be used and should be assume to be questionable. -// /// -// EarlyReturn, - -// /// -// /// Indicates a subsystem that validates data entered by the user. -// /// -// /// -// /// Errors are not reported, but are rather stored for later display. This may be reconsidered -// /// if we keep track of which errors have been reported. -// /// -// Validate, - -// /// -// /// Indicates a subsystem that executes using data entered by the user. The only known case is -// /// the Invocation subsystem. -// /// -// Execute, - -// /// -// /// Indicates a subsystem that runs as the CLI part of processing is ending. ErrorReportingSubsystem -// /// is an example, although we may rethink when errors are displayed. -// /// -// /// -// /// This is separate from the TearDown step, which is avaiable to all subsystems. -// /// -// Finish, -//} - - + internal TSubsystem? Subsystem + { + get => subsystem; + set + { + CliSubsystem = value; + subsystem = value; + } + } +} From b878eba40c47425854ebfa77ac5cc0d3c2da0123 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sun, 14 Jul 2024 14:13:43 -0400 Subject: [PATCH 099/150] Updated on my review-cleanup, better comments, readeonly. Also updated #2410 to describe --- src/System.CommandLine.Subsystems/Pipeline.cs | 50 +++++++++---------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 598edd64b1..7b5e1da941 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -9,16 +9,14 @@ namespace System.CommandLine; public partial class Pipeline { - // TODO: Consider more phases that have obvious meanings, like first and last - private PipelinePhase diagramPhase = new(SubsystemKind.Diagram); - private PipelinePhase completionPhase = new(SubsystemKind.Completion); - private PipelinePhase helpPhase = new(SubsystemKind.Help); - private PipelinePhase versionPhase = new(SubsystemKind.Version); - private PipelinePhase validationPhase = new(SubsystemKind.Validation); - private PipelinePhase invocationPhase = new(SubsystemKind.Invocation); - private PipelinePhase errorReportingPhase = new(SubsystemKind.ErrorReporting); - // TODO: Consider this naming as it sounds like it is a finishing phase - private readonly IEnumerable phases = []; + private readonly PipelinePhase diagramPhase = new(SubsystemKind.Diagram); + private readonly PipelinePhase completionPhase = new(SubsystemKind.Completion); + private readonly PipelinePhase helpPhase = new(SubsystemKind.Help); + private readonly PipelinePhase versionPhase = new(SubsystemKind.Version); + private readonly PipelinePhase validationPhase = new(SubsystemKind.Validation); + private readonly PipelinePhase invocationPhase = new(SubsystemKind.Invocation); + private readonly PipelinePhase errorReportingPhase = new(SubsystemKind.ErrorReporting); + private readonly IEnumerable phases; /// /// Creates an instance of the pipeline using standard features. @@ -37,8 +35,7 @@ public static Pipeline Create(HelpSubsystem? help = null, CompletionSubsystem? completion = null, DiagramSubsystem? diagram = null, ErrorReportingSubsystem? errorReporting = null) - { - Pipeline pipeline = new() + => new() { Help = help ?? new HelpSubsystem(), Version = version ?? new VersionSubsystem(), @@ -47,10 +44,6 @@ public static Pipeline Create(HelpSubsystem? help = null, ErrorReporting = errorReporting ?? new ErrorReportingSubsystem(), }; - - return pipeline; - } - /// /// Creates an instance of the pipeline with no features. Use this if you want to explicitly add features. /// @@ -88,21 +81,21 @@ public bool ResponseFilesEnabled } /// - /// Adds a subsystem. Use the property for the subsystem to replace the standard property. Use this method if you want an an additional subsystem. + /// Adds a subsystem. /// /// The subsystem to add. /// indicates that the subsystem should run before all other subsystems in the phase, and indicates it should run after other subsystems. The default is . /// /// - /// The phase in which the subsystem runs is determined by the subsystem. + /// The phase in which the subsystem runs is determined by the subsystem's 'Kind' property. + ///
+ /// To replace one of hte standard subsystems, use the `Pipeline.(subsystem)` property, such as `myPipeline.Help = new OtherHelp();` ///
public void AddSubsystem(CliSubsystem subsystem, PhaseTiming timing = PhaseTiming.Before) { switch (subsystem.Kind) { - // TODO: Determine how Other should work. Do we have a kind and a phase? Perhaps just for Other subsystems. I think it is helpful to know it is something unforeseen case SubsystemKind.Other: - break; case SubsystemKind.Response: case SubsystemKind.Value: throw new InvalidOperationException($"You cannot add subsystems to {subsystem.Kind}"); @@ -118,6 +111,12 @@ public void AddSubsystem(CliSubsystem subsystem, PhaseTiming timing = PhaseTimin case SubsystemKind.Version: versionPhase.AddSubsystem(subsystem, timing); break; + case SubsystemKind.Validation: + validationPhase.AddSubsystem(subsystem, timing); + break; + case SubsystemKind.Invocation: + invocationPhase.AddSubsystem(subsystem, timing); + break; case SubsystemKind.ErrorReporting: errorReportingPhase.AddSubsystem(subsystem, timing); break; @@ -140,7 +139,6 @@ public CompletionSubsystem? Completion { get => completionPhase.Subsystem; set => completionPhase.Subsystem = value; - } /// @@ -150,7 +148,6 @@ public HelpSubsystem? Help { get => helpPhase.Subsystem; set => helpPhase.Subsystem = value; - } /// @@ -171,6 +168,7 @@ public ErrorReportingSubsystem? ErrorReporting set => errorReportingPhase.Subsystem = value; } + // TODO: Consider whether replacing the validation subsystem is valuable /// /// Sets or gets the validation subsystem /// @@ -180,7 +178,7 @@ public ValidationSubsystem? Validation set => validationPhase.Subsystem = value; } - + // TODO: Consider whether replacing the invocation subsystem is valuable /// /// Sets or gets the invocation subsystem /// @@ -190,15 +188,13 @@ public InvocationSubsystem? Invocation set => invocationPhase.Subsystem = value; } - - // TODO: Are there use cases to replace this? Do we want new default values to require a new ValueSubsystem, which would block getting response providers from two sources. /// - /// Sets or gets the value subsystem which manages entered and default values. + /// Gets the value subsystem which manages entered and default values. /// public ValueSubsystem Value { get; } /// - /// Sets or gets the response file subsystem + /// Gets the response file subsystem /// public ResponseSubsystem Response { get; } From dabfb02306f3aafebe9691b2b75d6a3fd25fa5dc Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Wed, 17 Jul 2024 20:23:20 -0400 Subject: [PATCH 100/150] Response to PR Review meeting --- .../CompletionSubsystem.cs | 3 +- .../Directives/DiagramSubsystem.cs | 170 +++++++++--------- .../Directives/DirectiveSubsystem.cs | 2 +- .../Directives/ResponseSubsystem.cs | 35 ++-- src/System.CommandLine.Subsystems/Pipeline.cs | 33 ++-- .../{PhaseTiming.cs => AddToPhaseBehavior.cs} | 7 +- .../Subsystems/CliSubsystem.cs | 3 +- .../Subsystems/PipelinePhase.cs | 29 ++- 8 files changed, 134 insertions(+), 148 deletions(-) rename src/System.CommandLine.Subsystems/Subsystems/{PhaseTiming.cs => AddToPhaseBehavior.cs} (71%) diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index 75777ec2f2..434d4e1934 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -3,14 +3,13 @@ using System.CommandLine.Subsystems; using System.CommandLine.Subsystems.Annotations; -using System.Reflection.Metadata.Ecma335; namespace System.CommandLine; public class CompletionSubsystem : CliSubsystem { public CompletionSubsystem(IAnnotationProvider? annotationProvider = null) - : base(CompletionAnnotations.Prefix, SubsystemKind.Completion, annotationProvider) + : base(CompletionAnnotations.Prefix, SubsystemKind.Completion, annotationProvider) { } // TODO: Figure out trigger for completions diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 2381ed6e49..104d0c3f07 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -70,113 +70,113 @@ private static void Diagram( */ // TODO: Directives /* - switch (symbolResult) - { - case DirectiveResult { Directive: not DiagramDirective }: - break; - */ - - // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives - /* - case ArgumentResult argumentResult: + switch (symbolResult) { - var includeArgumentName = - argumentResult.Argument.FirstParent!.Symbol is CliCommand { HasArguments: true, Arguments.Count: > 1 }; - - if (includeArgumentName) - { - builder.Append("[ "); - builder.Append(argumentResult.Argument.Name); - builder.Append(' '); - } + case DirectiveResult { Directive: not DiagramDirective }: + break; + */ - if (argumentResult.Argument.Arity.MaximumNumberOfValues > 0) - { - ArgumentConversionResult conversionResult = argumentResult.GetArgumentConversionResult(); - switch (conversionResult.Result) + // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives + /* + case ArgumentResult argumentResult: { - case ArgumentConversionResultType.NoArgument: - break; - case ArgumentConversionResultType.Successful: - switch (conversionResult.Value) + var includeArgumentName = + argumentResult.Argument.FirstParent!.Symbol is CliCommand { HasArguments: true, Arguments.Count: > 1 }; + + if (includeArgumentName) + { + builder.Append("[ "); + builder.Append(argumentResult.Argument.Name); + builder.Append(' '); + } + + if (argumentResult.Argument.Arity.MaximumNumberOfValues > 0) + { + ArgumentConversionResult conversionResult = argumentResult.GetArgumentConversionResult(); + switch (conversionResult.Result) { - case string s: - builder.Append($"<{s}>"); + case ArgumentConversionResultType.NoArgument: break; + case ArgumentConversionResultType.Successful: + switch (conversionResult.Value) + { + case string s: + builder.Append($"<{s}>"); + break; + + case IEnumerable items: + builder.Append('<'); + builder.Append( + string.Join("> <", + items.Cast().ToArray())); + builder.Append('>'); + break; + + default: + builder.Append('<'); + builder.Append(conversionResult.Value); + builder.Append('>'); + break; + } - case IEnumerable items: - builder.Append('<'); - builder.Append( - string.Join("> <", - items.Cast().ToArray())); - builder.Append('>'); break; - default: + default: // failures builder.Append('<'); - builder.Append(conversionResult.Value); + builder.Append(string.Join("> <", symbolResult.Tokens.Select(t => t.Value))); builder.Append('>'); + break; } + } - break; - - default: // failures - builder.Append('<'); - builder.Append(string.Join("> <", symbolResult.Tokens.Select(t => t.Value))); - builder.Append('>'); + if (includeArgumentName) + { + builder.Append(" ]"); + } - break; + break; } - } - - if (includeArgumentName) - { - builder.Append(" ]"); - } - - break; - } - default: - { - OptionResult? optionResult = symbolResult as OptionResult; - - if (optionResult is { Implicit: true }) - { - builder.Append('*'); - } + default: + { + OptionResult? optionResult = symbolResult as OptionResult; + + if (optionResult is { Implicit: true }) + { + builder.Append('*'); + } + + builder.Append("[ "); + + if (optionResult is not null) + { + builder.Append(optionResult.IdentifierToken?.Value ?? optionResult.Option.Name); + } + else + { + builder.Append(((CommandResult)symbolResult).IdentifierToken.Value); + } + + foreach (SymbolResult child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) + { + if (child is ArgumentResult arg && + (arg.Argument.ValueType == typeof(bool) || + arg.Argument.Arity.MaximumNumberOfValues == 0)) + { + continue; + } - builder.Append("[ "); + builder.Append(' '); - if (optionResult is not null) - { - builder.Append(optionResult.IdentifierToken?.Value ?? optionResult.Option.Name); - } - else - { - builder.Append(((CommandResult)symbolResult).IdentifierToken.Value); - } + Diagram(builder, child, parseResult); + } - foreach (SymbolResult child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) - { - if (child is ArgumentResult arg && - (arg.Argument.ValueType == typeof(bool) || - arg.Argument.Arity.MaximumNumberOfValues == 0)) - { - continue; + builder.Append(" ]"); + break; } - - builder.Append(' '); - - Diagram(builder, child, parseResult); } - - builder.Append(" ]"); - break; } } -} -} */ } diff --git a/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs index 8616d71491..efb51bceee 100644 --- a/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs @@ -13,7 +13,7 @@ public abstract class DirectiveSubsystem : CliSubsystem public string Id { get; } public Location? Location { get; private set; } - public DirectiveSubsystem(string name, SubsystemKind kind, IAnnotationProvider? annotationProvider = null, string? id = null) + public DirectiveSubsystem(string name, SubsystemKind kind, IAnnotationProvider? annotationProvider = null, string? id = null) : base(name, kind, annotationProvider: annotationProvider) { Id = id ?? name; diff --git a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs index ebd31cf150..c626348e28 100644 --- a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs @@ -16,26 +16,25 @@ protected internal override void Initialize(InitializationContext context) public (List? tokens, List? errors) Replacer(string responseSourceName) { - if (Enabled) + if (!Enabled) { - try - { - // TODO: Include checks from previous system. - var contents = File.ReadAllText(responseSourceName); - return (CliParser.SplitCommandLine(contents).ToList(), null); - } - catch - { - // TODO: Switch to proper errors - return (null, - errors: - [ - $"Failed to open response file {responseSourceName}" - ]); - } + return ([responseSourceName], null); + } + try + { + // TODO: Include checks from previous system. + var contents = File.ReadAllText(responseSourceName); + return (CliParser.SplitCommandLine(contents).ToList(), null); + } + catch + { + // TODO: Switch to proper errors + return (null, + errors: + [ + $"Failed to open response file {responseSourceName}" + ]); } - // TODO: Confirm this is not an error state - return ([responseSourceName], null); } // TODO: File handling from previous system - ensure these checks are done (note: no tests caught these oversights diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 7b5e1da941..a223113547 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -21,11 +21,11 @@ public partial class Pipeline /// /// Creates an instance of the pipeline using standard features. /// - /// A help subsystem to replace the standard one. To add a subsystem, use AddSubsystem. - /// A help subsystem to replace the standard one. To add a subsystem, use AddSubsystem. - /// A help subsystem to replace the standard one. To add a subsystem, use AddSubsystem. - /// A help subsystem to replace the standard one. To add a subsystem, use AddSubsystem. - /// A help subsystem to replace the standard one. To add a subsystem, use AddSubsystem. + /// A help subsystem to replace the standard one. To add a subsystem, use + /// A help subsystem to replace the standard one. To add a subsystem, use + /// A help subsystem to replace the standard one. To add a subsystem, use + /// A help subsystem to replace the standard one. To add a subsystem, use + /// A help subsystem to replace the standard one. To add a subsystem, use /// A new pipeline. /// /// Currently, the standard , , and cannot be replaced. is disabled by default. @@ -89,9 +89,9 @@ public bool ResponseFilesEnabled /// /// The phase in which the subsystem runs is determined by the subsystem's 'Kind' property. ///
- /// To replace one of hte standard subsystems, use the `Pipeline.(subsystem)` property, such as `myPipeline.Help = new OtherHelp();` + /// To replace one of the standard subsystems, use the `Pipeline.(subsystem)` property, such as `myPipeline.Help = new OtherHelp();` ///
- public void AddSubsystem(CliSubsystem subsystem, PhaseTiming timing = PhaseTiming.Before) + public void AddSubsystem(CliSubsystem subsystem, AddToPhaseBehavior timing = AddToPhaseBehavior.SubsystemRecommendation) { switch (subsystem.Kind) { @@ -111,6 +111,8 @@ public void AddSubsystem(CliSubsystem subsystem, PhaseTiming timing = PhaseTimin case SubsystemKind.Version: versionPhase.AddSubsystem(subsystem, timing); break; + // You can add Validation and Invocation subsystems, but you can't remove the core. + // Other things may need to be run in the phase. case SubsystemKind.Validation: validationPhase.AddSubsystem(subsystem, timing); break; @@ -172,21 +174,13 @@ public ErrorReportingSubsystem? ErrorReporting /// /// Sets or gets the validation subsystem /// - public ValidationSubsystem? Validation - { - get => validationPhase.Subsystem; - set => validationPhase.Subsystem = value; - } + public ValidationSubsystem? Validation { get; } // TODO: Consider whether replacing the invocation subsystem is valuable /// /// Sets or gets the invocation subsystem /// - public InvocationSubsystem? Invocation - { - get => invocationPhase.Subsystem; - set => invocationPhase.Subsystem = value; - } + public InvocationSubsystem? Invocation { get; } /// /// Gets the value subsystem which manages entered and default values. @@ -222,6 +216,7 @@ public PipelineResult Execute(ParseResult parseResult, string rawInput, ConsoleH // TODO: Allow subsystems to control short-circuiting foreach (var subsystem in phase.GetSubsystems()) { + // TODO: RunEvenIfAlreadyHandled needs more thought and laying out the scenarios if (subsystem is not null && (!pipelineResult.AlreadyHandled || subsystem.RunsEvenIfAlreadyHandled)) { subsystem.ExecuteIfNeeded(pipelineResult); @@ -250,7 +245,7 @@ protected virtual void InitializeSubsystems(InitializationContext context) // TODO: Allow subsystems to control short-circuiting? Not sure we need that for initialization foreach (var subsystem in phase.GetSubsystems()) { - subsystem?.Initialize(context); + subsystem.Initialize(context); } } } @@ -269,7 +264,7 @@ protected virtual void TearDownSubsystems(PipelineResult pipelineResult) // TODO: Allow subsystems to control short-circuiting? Not sure we need that for teardown foreach (var subsystem in phase.GetSubsystems()) { - subsystem?.TearDown(pipelineResult); + subsystem.TearDown(pipelineResult); } } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/PhaseTiming.cs b/src/System.CommandLine.Subsystems/Subsystems/AddToPhaseBehavior.cs similarity index 71% rename from src/System.CommandLine.Subsystems/Subsystems/PhaseTiming.cs rename to src/System.CommandLine.Subsystems/Subsystems/AddToPhaseBehavior.cs index ea07f16f85..45d8866fed 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/PhaseTiming.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/AddToPhaseBehavior.cs @@ -3,8 +3,9 @@ namespace System.CommandLine.Subsystems; -public enum PhaseTiming +public enum AddToPhaseBehavior { - Before = 0, - After + SubsystemRecommendation = 0, + Prepend, + Append } diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index f4c785ae96..f48a27a015 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -12,7 +12,7 @@ namespace System.CommandLine.Subsystems; /// public abstract class CliSubsystem { - protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProvider? annotationProvider) + protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProvider? annotationProvider) { Name = name; _annotationProvider = annotationProvider; @@ -28,6 +28,7 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationPro /// Defines the kind of subsystem, such as help or version /// public SubsystemKind Kind { get; } + public AddToPhaseBehavior RecommendedAddToPhaseBehavior { get; } private readonly IAnnotationProvider? _annotationProvider; diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs b/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs index 9187c3dca4..220b3217a9 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs @@ -19,9 +19,10 @@ internal class PipelinePhase(SubsystemKind kind) /// /// Adding a subsystem that is not of the normal phase type is expected and OK. /// - public void AddSubsystem(CliSubsystem subsystem, PhaseTiming timing = PhaseTiming.Before) + internal void AddSubsystem(CliSubsystem subsystem, AddToPhaseBehavior timing) { - List? addToList = timing == PhaseTiming.Before + timing = timing == AddToPhaseBehavior.SubsystemRecommendation ? subsystem.RecommendedAddToPhaseBehavior : timing; + List? addToList = timing == AddToPhaseBehavior.Prepend ? CreateBeforeIfNeeded() : CreateAfterIfNeeded(); @@ -40,26 +41,16 @@ private List CreateAfterIfNeeded() return after; } - public IEnumerable GetSubsystems() - { - List ret = CliSubsystem is null - ? [] - : [CliSubsystem]; - if (before is not null) - { - // TODO: Confirm that we want to reverse the before list. - ret.AddRange(((IEnumerable)before).Reverse()); - } - if (after is not null) - { - ret.AddRange(after); - } - return ret; - } + public IEnumerable GetSubsystems() + => [ + .. (before is null ? [] : before), + .. (CliSubsystem is null ? new List { } : [CliSubsystem]), + .. (after is null ? [] : after) + ]; } /// -/// This struct manages one phase. The most common case is that it is empty, and the most complicated +/// This manages one phase. The most common case is that it is empty, and the most complicated /// case of several items before, and several items after will be quite rare. /// /// From 397ab7bb2be9a9a37e8892a836586394ff36fe8a Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Tue, 6 Aug 2024 18:39:25 -0400 Subject: [PATCH 101/150] Added ParseResult.CommandValueResult, fixed tests, and... Removed only direct use of SymbolResultTree in ParseResult Marked the things that need to be removed/replaced to remove indirect usage (SymbolResult derived classes) Some cleanup --- src/System.CommandLine.Tests/ParserTests.cs | 48 +++++----- src/System.CommandLine/ParseResult.cs | 14 ++- .../Parsing/ArgumentResult.cs | 1 - .../Parsing/CommandResult.cs | 92 +++++++++++-------- .../Parsing/CommandValueResult.cs | 2 +- .../Parsing/ParseOperation.cs | 2 +- 6 files changed, 91 insertions(+), 68 deletions(-) diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 899552c804..98841082a2 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -139,9 +139,9 @@ public void Option_short_forms_can_be_bundled() var result = CliParser.Parse(command, "the-command -xyz"); - result.CommandResult - .Children - .Select(o => ((OptionResult)o).Option.Name) + result.CommandValueResult + .ValueResults + .Select(o => o.ValueSymbol.Name) .Should() .BeEquivalentTo("-x", "-y", "-z"); } @@ -1710,11 +1710,11 @@ public void CommandResult_contains_argument_ValueResults() var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandResult = parseResult.CommandResult; - commandResult.ValueResults.Should().HaveCount(2); - var result1 = commandResult.ValueResults[0]; + var commandValueResult = parseResult.CommandValueResult; + commandValueResult.ValueResults.Should().HaveCount(2); + var result1 = commandValueResult.ValueResults.First(); result1.GetValue().Should().Be("Kirk"); - var result2 = commandResult.ValueResults[1]; + var result2 = commandValueResult.ValueResults.Skip(1).First(); result2.GetValue().Should().Be("Spock"); } @@ -1735,11 +1735,11 @@ public void CommandResult_contains_option_ValueResults() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); - var commandResult = parseResult.CommandResult; - commandResult.ValueResults.Should().HaveCount(2); - var result1 = commandResult.ValueResults[0]; + var commandValueResult = parseResult.CommandValueResult; + commandValueResult.ValueResults.Should().HaveCount(2); + var result1 = commandValueResult.ValueResults[0]; result1.GetValue().Should().Be("Kirk"); - var result2 = commandResult.ValueResults[1]; + var result2 = commandValueResult.ValueResults[1]; result2.GetValue().Should().Be("Spock"); } @@ -1763,9 +1763,9 @@ public void Location_in_ValueResult_correct_for_arguments() var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandResult = parseResult.CommandResult; - var result1 = commandResult.ValueResults[0]; - var result2 = commandResult.ValueResults[1]; + var commandValueResult = parseResult.CommandValueResult; + var result1 = commandValueResult.ValueResults[0]; + var result2 = commandValueResult.ValueResults[1]; result1.Locations.Single().Should().Be(expectedLocation1); result2.Locations.Single().Should().Be(expectedLocation2); } @@ -1790,9 +1790,9 @@ public void Location_in_ValueResult_correct_for_options() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); - var commandResult = parseResult.CommandResult; - var result1 = commandResult.ValueResults[0]; - var result2 = commandResult.ValueResults[1]; + var commandValueResult = parseResult.CommandValueResult; + var result1 = commandValueResult.ValueResults[0]; + var result2 = commandValueResult.ValueResults[1]; result1.Locations.Single().Should().Be(expectedLocation1); result2.Locations.Single().Should().Be(expectedLocation2); } @@ -1817,8 +1817,8 @@ public void Location_offsets_in_ValueResult_correct_for_arguments() var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandResult = parseResult.CommandResult; - var result1 = commandResult.ValueResults.Single(); + var commandValueResult = parseResult.CommandValueResult; + var result1 = commandValueResult.ValueResults.Single(); result1.Locations.First().Should().Be(expectedLocation1); result1.Locations.Skip(1).Single().Should().Be(expectedLocation2); } @@ -1841,8 +1841,8 @@ public void Location_offsets_in_ValueResult_correct_for_options() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt1 Spock"); - var commandResult = parseResult.CommandResult; - var result1 = commandResult.ValueResults.Single(); + var commandValueResult = parseResult.CommandValueResult; + var result1 = commandValueResult.ValueResults.Single(); result1.Locations.First().Should().Be(expectedLocation1); result1.Locations.Skip(1).Single().Should().Be(expectedLocation2); } @@ -1867,9 +1867,9 @@ public void Location_offset_correct_when_colon_or_equal_used() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1:Kirk --opt11=Spock"); - var commandResult = parseResult.CommandResult; - var result1 = commandResult.ValueResults[0]; - var result2 = commandResult.ValueResults[1]; + var commandValueResult = parseResult.CommandValueResult; + var result1 = commandValueResult.ValueResults[0]; + var result2 = commandValueResult.ValueResults[1]; result1.Locations.Single().Should().Be(expectedLocation1); result2.Locations.Single().Should().Be(expectedLocation2); } diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 6a15a0ba61..2728ef67ff 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -17,6 +17,7 @@ public sealed class ParseResult private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); private SymbolLookupByName? symbolLookupByName = null; + // TODO: Remove usage and remove private readonly CommandResult _rootCommandResult; // TODO: unmatched tokens, invocation, completion /* @@ -28,10 +29,11 @@ public sealed class ParseResult internal ParseResult( CliConfiguration configuration, - // TODO: determine how rootCommandResult and commandResult differ + // TODO: Remove RootCommandResult - it is the root of the CommandValueResult ancestors (fix that) CommandResult rootCommandResult, + // TODO: Replace with CommandValueResult and remove CommandResult CommandResult commandResult, - SymbolResultTree symbolResultTree, + IReadOnlyDictionary valueResultDictionary, /* List tokens, */ @@ -50,7 +52,8 @@ internal ParseResult( Configuration = configuration; _rootCommandResult = rootCommandResult; CommandResult = commandResult; - valueResultDictionary = symbolResultTree.BuildValueResultDictionary(); + CommandValueResult = commandResult.CommandValueResult; + this.valueResultDictionary = valueResultDictionary; // TODO: invocation /* _action = action; @@ -98,8 +101,11 @@ internal ParseResult( /// /// A result indicating the command specified in the command line input. /// + // TODO: Update SymbolLookupByName to use CommandValueResult, then remove internal CommandResult CommandResult { get; } + public CommandValueResult CommandValueResult { get; } + /// /// The configuration used to produce the parse result. /// @@ -108,6 +114,7 @@ internal ParseResult( /// /// Gets the root command result. /// + /// TODO: Update usage and then remove internal CommandResult RootCommandResult => _rootCommandResult; /// @@ -215,6 +222,7 @@ CommandLineText is null ? result : null; + // TODO: Update tests and remove all use of things deriving from SymbolResult from this class /// /// Gets the result, if any, for the specified argument. /// diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/ArgumentResult.cs index 9e2248b870..8dc87d16a7 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/ArgumentResult.cs @@ -33,7 +33,6 @@ public ValueResult ValueResult // TODO: Make sure errors are added var conversionValue = GetArgumentConversionResult().Value; var locations = Tokens.Select(token => token.Location).ToArray(); - //TODO: Remove this wrapper later _valueResult = new ValueResult(Argument, conversionValue, locations, ArgumentResult.GetValueResultOutcome(GetArgumentConversionResult()?.Result)); // null is temporary here } return _valueResult; diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CommandResult.cs index f486794eba..2ed58a40cc 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CommandResult.cs @@ -38,7 +38,23 @@ internal CommandResult( /// public IEnumerable Children => SymbolResultTree.GetChildren(this); - public IReadOnlyList ValueResults => Children.Select(GetValueResult).OfType().ToList(); + private CommandValueResult? commandValueResult; + public CommandValueResult CommandValueResult + { + get + { + if (commandValueResult is null) + { + var parent = Parent is CommandResult commandResult + ? commandResult.CommandValueResult + : null; + commandValueResult = new CommandValueResult(Command, parent); + } + // Reset unless we put tests in place to ensure it is not called in error handling before SymbolTree processing is complete + commandValueResult.ValueResults = Children.Select(GetValueResult).OfType().ToList(); + return commandValueResult; + } + } private ValueResult? GetValueResult(SymbolResult symbolResult) => symbolResult switch @@ -59,30 +75,30 @@ internal void Validate(bool completeValidation) { if (completeValidation) { -// TODO: invocation -// if (Command.Action is null && Command.HasSubcommands) + // TODO: invocation + // if (Command.Action is null && Command.HasSubcommands) if (Command.HasSubcommands) { SymbolResultTree.InsertFirstError( new ParseError(LocalizationResources.RequiredCommandWasNotProvided(), this)); } -// TODO: validators -/* - 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; - } - } -*/ + // TODO: validators + /* + 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; + } + } + */ } // TODO: Validation @@ -104,8 +120,8 @@ private void ValidateOptions(bool completeValidation) { var option = options[i]; -// TODO: VersionOption, recursive options -// if (!completeValidation && !(option.Recursive || option.Argument.HasDefaultValue || option is VersionOption)) + // TODO: VersionOption, recursive options + // if (!completeValidation && !(option.Recursive || option.Argument.HasDefaultValue || option is VersionOption)) if (!completeValidation && !option.Argument.HasDefaultValue) { continue; @@ -148,23 +164,23 @@ private void ValidateOptions(bool completeValidation) continue; } -// TODO: validators -/* - if (optionResult.Option.HasValidators) - { - int errorsBefore = SymbolResultTree.ErrorCount; - - for (var j = 0; j < optionResult.Option.Validators.Count; j++) - { - optionResult.Option.Validators[j](optionResult); - } - - if (errorsBefore != SymbolResultTree.ErrorCount) - { - continue; - } - } -*/ + // TODO: validators + /* + if (optionResult.Option.HasValidators) + { + int errorsBefore = SymbolResultTree.ErrorCount; + + for (var j = 0; j < optionResult.Option.Validators.Count; j++) + { + optionResult.Option.Validators[j](optionResult); + } + + if (errorsBefore != SymbolResultTree.ErrorCount) + { + continue; + } + } + */ // TODO: Ensure all argument conversions are run for entered values /* diff --git a/src/System.CommandLine/Parsing/CommandValueResult.cs b/src/System.CommandLine/Parsing/CommandValueResult.cs index 60f403b5c1..a054f47506 100644 --- a/src/System.CommandLine/Parsing/CommandValueResult.cs +++ b/src/System.CommandLine/Parsing/CommandValueResult.cs @@ -27,7 +27,7 @@ internal CommandValueResult(CliCommand command, CommandValueResult? parent = nul /// /// The ValueResult instances for user entered data. This is a sparse list. /// - public IEnumerable ValueResults { get; } = new List(); + public IReadOnlyList ValueResults { get; internal set; } = []; /// /// The CliCommand that the result is for. diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 8e972d2728..9b886f70ce 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -85,7 +85,7 @@ internal ParseResult Parse() _configuration, _rootCommandResult, _innermostCommandResult, - _rootCommandResult.SymbolResultTree, + _rootCommandResult.SymbolResultTree.BuildValueResultDictionary(), /* _tokens, */ From 90fb5c31dfcaa6d8e847b43da58dfd31cb0c23a4 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 10 Aug 2024 06:40:07 -0400 Subject: [PATCH 102/150] Fixed whitespace, commented out dead code --- .../Parsing/CommandResult.cs | 70 +++++++++---------- .../Parsing/OptionResult.cs | 3 + .../Parsing/ParseOperation.cs | 3 + .../Parsing/SymbolResult.cs | 3 + 4 files changed, 43 insertions(+), 36 deletions(-) diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CommandResult.cs index 2ed58a40cc..742d33128c 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CommandResult.cs @@ -67,16 +67,19 @@ public CommandValueResult CommandValueResult /// public override string ToString() => $"{nameof(CommandResult)}: {IdentifierToken.Value} {string.Join(" ", Tokens.Select(t => t.Value))}"; + // TODO: DefaultValues + /* internal override bool UseDefaultValueFor(ArgumentResult argumentResult) => argumentResult.Argument.HasDefaultValue && argumentResult.Tokens.Count == 0; + */ + // TODO: Validation + /* /// Only the inner most command goes through complete validation. internal void Validate(bool completeValidation) { if (completeValidation) { - // TODO: invocation - // if (Command.Action is null && Command.HasSubcommands) if (Command.HasSubcommands) { SymbolResultTree.InsertFirstError( @@ -84,21 +87,19 @@ internal void Validate(bool completeValidation) } // TODO: validators - /* - 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.HasValidators) + { + int errorCountBefore = SymbolResultTree.ErrorCount; + for (var i = 0; i < Command.Validators.Count; i++) + { + Command.Validators[i](this); + } + + if (SymbolResultTree.ErrorCount != errorCountBefore) + { + return; + } + } } // TODO: Validation @@ -121,7 +122,7 @@ private void ValidateOptions(bool completeValidation) var option = options[i]; // TODO: VersionOption, recursive options - // if (!completeValidation && !(option.Recursive || option.Argument.HasDefaultValue || option is VersionOption)) + // if (!completeValidation && !(option.Recursive || option.Argument.HasDefaultValue || option is VersionOption)) if (!completeValidation && !option.Argument.HasDefaultValue) { continue; @@ -165,27 +166,23 @@ private void ValidateOptions(bool completeValidation) } // TODO: validators - /* - if (optionResult.Option.HasValidators) - { - int errorsBefore = SymbolResultTree.ErrorCount; - - for (var j = 0; j < optionResult.Option.Validators.Count; j++) - { - optionResult.Option.Validators[j](optionResult); - } - - if (errorsBefore != SymbolResultTree.ErrorCount) - { - continue; - } - } - */ + if (optionResult.Option.HasValidators) + { + int errorsBefore = SymbolResultTree.ErrorCount; + + for (var j = 0; j < optionResult.Option.Validators.Count; j++) + { + optionResult.Option.Validators[j](optionResult); + } + + if (errorsBefore != SymbolResultTree.ErrorCount) + { + continue; + } + } // TODO: Ensure all argument conversions are run for entered values - /* _ = argumentResult.GetArgumentConversionResult(); - */ } } @@ -226,5 +223,6 @@ private void ValidateArguments(bool completeValidation) _ = argumentResult.GetArgumentConversionResult(); } } + */ } } diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/OptionResult.cs index 9bfa62babb..e01ca8107d 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/OptionResult.cs @@ -93,6 +93,9 @@ internal bool IsArgumentLimitReached internal ArgumentConversionResult ArgumentConversionResult => _argumentConversionResult ??= GetResult(Option.Argument)!.GetArgumentConversionResult(); + // TODO: Default values + /* internal override bool UseDefaultValueFor(ArgumentResult argument) => Implicit; + */ } } diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 9b886f70ce..04bf5e31e2 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -389,6 +389,8 @@ private void AddCurrentTokenToUnmatched() _symbolResultTree.AddUnmatchedToken(CurrentToken, _innermostCommandResult, _rootCommandResult); } + // TODO: Validation + /* private void Validate() { // Only the inner most command goes through complete validation, @@ -403,5 +405,6 @@ private void Validate() currentResult = currentResult.Parent as CommandResult; } } + */ } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/SymbolResult.cs index f32bec67c4..e1b49b637d 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/SymbolResult.cs @@ -156,6 +156,9 @@ public IEnumerable Errors } */ + // TODO: DefaultValues + /* internal virtual bool UseDefaultValueFor(ArgumentResult argumentResult) => false; + */ } } From 5bb4acbc029f9fef50c5dbddbbb555d89c9deeaa Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 10 Aug 2024 06:47:07 -0400 Subject: [PATCH 103/150] Added blank line eof --- src/System.CommandLine/Parsing/ParseOperation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 04bf5e31e2..63f5b03375 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -407,4 +407,4 @@ private void Validate() } */ } -} \ No newline at end of file +} From 8b4d3717e141e1a17611c02b557ca05834629916 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 10 Aug 2024 11:08:16 -0400 Subject: [PATCH 104/150] Remove Version AnnotationId When a version is specified, it is not treated as an annotation --- .../Subsystems/Annotations/VersionAnnotations.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs index 463954e2da..3672894c24 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/VersionAnnotations.cs @@ -9,6 +9,4 @@ namespace System.CommandLine.Subsystems.Annotations; public static class VersionAnnotations { public static string Prefix { get; } = nameof(SubsystemKind.Version); - - public static AnnotationId Version { get; } = new(Prefix, nameof(Version)); } From 603bd309287f9762383c60113e78834e4be68581 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 10 Aug 2024 11:13:08 -0400 Subject: [PATCH 105/150] Added comment explaining HelpOption usage --- src/System.CommandLine.Subsystems/HelpSubsystem.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index 67af643177..84626e8f21 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -18,6 +18,14 @@ namespace System.CommandLine; public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) : CliSubsystem(HelpAnnotations.Prefix, SubsystemKind.Help, annotationProvider) { + /// + /// Gets the help option, which allows the user to customize + /// + /// + /// By design, the user can modify the help option but not replace it. This allows us to + /// do the fastest possible lookup of whether help was called, and we aren't clear why + /// the option would need to be replaced + /// public CliOption HelpOption { get; } = new CliOption("--help", ["-h"]) { // TODO: Why don't we accept bool like any other bool option? From 098913cab9190e2c9f79d82f7b47a69bda8d7d86 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 10 Aug 2024 11:31:57 -0400 Subject: [PATCH 106/150] Added thoughts from conversation with Chet --- src/System.CommandLine.Subsystems/CompletionSubsystem.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index 434d4e1934..04eea0ce16 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -6,6 +6,15 @@ namespace System.CommandLine; +// Notes from Chet's work on static shells and further thoughts +// - Chet has work he can later upstream for completion script creation - static when it can be +// - Completions need to know - what it is, whether it is static or dynamic, and how many items would be in the list. +// - Not sure whether these need to be in the Completer or the trait +// - Validation can have many validators per type. Completions may need to have a single one. +// - Probably two steps - determining the available values and matching the current word +// - The code in CompletionContext of main/Extended to get current word requires tokens and is pretty gnarly +// - File and directory are special as they can get handed off to shell ro the work + public class CompletionSubsystem : CliSubsystem { public CompletionSubsystem(IAnnotationProvider? annotationProvider = null) From 83855a57d9d4e8facd397521793b7ce53e3237b6 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 10 Aug 2024 11:51:57 -0400 Subject: [PATCH 107/150] Add Error tracking to PipelineResult --- .../PipelineResult.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/System.CommandLine.Subsystems/PipelineResult.cs b/src/System.CommandLine.Subsystems/PipelineResult.cs index 1b236fb9ec..8762a68720 100644 --- a/src/System.CommandLine.Subsystems/PipelineResult.cs +++ b/src/System.CommandLine.Subsystems/PipelineResult.cs @@ -1,10 +1,13 @@ // 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.Parsing; + namespace System.CommandLine; public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) { + private readonly List errors = []; public ParseResult? ParseResult { get; } = parseResult; public string RawInput { get; } = rawInput; public Pipeline Pipeline { get; } = pipeline ?? Pipeline.CreateEmpty(); @@ -13,6 +16,22 @@ public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? public bool AlreadyHandled { get; set; } public int ExitCode { get; set; } + public void AddErrors(IEnumerable errors) + { + if (errors is not null) + { + this.errors.AddRange(errors); + } + } + + public void AddError(ParseError error) + => errors.Add(error); + + public IEnumerable GetErrors(bool excludeParseErrors = false) + => excludeParseErrors || ParseResult is null + ? errors + : ParseResult.Errors.Concat(errors); + public void NotRun(ParseResult? parseResult) { // no op because defaults are false and 0 From acc2446e002a96cf7bfd19857781c9c1c54c2b8c Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 10 Aug 2024 12:35:01 -0400 Subject: [PATCH 108/150] Added CliDataSymbol to simplify code This will remove a ton of places where Option and Argument had to be separately handled --- src/System.CommandLine/CliArgument.cs | 44 ++++++++----------- src/System.CommandLine/CliDataSymbol.cs | 19 ++++++++ src/System.CommandLine/CliOption.cs | 36 ++++++++------- .../System.CommandLine.csproj | 1 + 4 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 src/System.CommandLine/CliDataSymbol.cs diff --git a/src/System.CommandLine/CliArgument.cs b/src/System.CommandLine/CliArgument.cs index 617f1d5b9b..3e28f7ce42 100644 --- a/src/System.CommandLine/CliArgument.cs +++ b/src/System.CommandLine/CliArgument.cs @@ -10,16 +10,17 @@ namespace System.CommandLine /// /// A symbol defining a value that can be passed on the command line to a command or option. /// - public abstract class CliArgument : CliSymbol + public abstract class CliArgument : CliDataSymbol { private ArgumentArity _arity; -// TODO: custom parser, completion, validators -/* + // TODO: custom parser, completion, validators + /* private TryConvertArgument? _convertArguments; private List>>? _completionSources = null; private List>? _validators = null; -*/ - private protected CliArgument(string name) : base(name, allowWhitespace: true) + */ + private protected CliArgument(string name) + : base(name, allowWhitespace: true) { } @@ -39,24 +40,24 @@ public ArgumentArity Arity } set => _arity = value; } -// TODO: help, completion -/* + // TODO: help, completion + /* /// /// The name used in help output to describe the argument. /// public string? HelpName { get; set; } -*/ + */ internal TryConvertArgument? ConvertArguments => ArgumentConverter.GetConverter(this); -// TODO: custom parsers -/* + // TODO: custom parsers + /* { get => _convertArguments ??= ArgumentConverter.GetConverter(this); set => _convertArguments = value; } -*/ + */ -// TODO: completion; -/* + // TODO: completion; + /* /// /// Gets the list of completion sources for the argument. /// @@ -94,13 +95,8 @@ public List>> CompletionSour return _completionSources; } } -*/ - /// - /// Gets or sets the that the argument's parsed tokens will be converted to. - /// - public abstract Type ValueType { get; } -/* TODO: validators + /* TODO: validators /// /// Provides a list of argument validators. Validators can be used /// to provide custom errors based on user input. @@ -108,7 +104,7 @@ public List>> CompletionSour public List> Validators => _validators ??= new (); internal bool HasValidators => (_validators?.Count ?? 0) > 0; -*/ + */ /// /// Gets the default value for the argument. /// @@ -124,8 +120,8 @@ public List>> CompletionSour /// Specifies if a default value is defined for the argument. /// public abstract bool HasDefaultValue { get; } -// TODO: completion -/* + // TODO: completion + /* /// public override IEnumerable GetCompletions(CompletionContext context) { @@ -134,10 +130,8 @@ public override IEnumerable GetCompletions(CompletionContext con .Distinct() .OrderBy(c => c.SortText, StringComparer.OrdinalIgnoreCase); } -*/ + */ /// public override string ToString() => $"{nameof(CliArgument)}: {Name}"; - - internal bool IsBoolean() => ValueType == typeof(bool) || ValueType == typeof(bool?); } } diff --git a/src/System.CommandLine/CliDataSymbol.cs b/src/System.CommandLine/CliDataSymbol.cs new file mode 100644 index 0000000000..8aa9069e34 --- /dev/null +++ b/src/System.CommandLine/CliDataSymbol.cs @@ -0,0 +1,19 @@ +// 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; + +public abstract class CliDataSymbol : CliSymbol +{ + protected CliDataSymbol(string name, bool allowWhitespace = false) + : base(name, allowWhitespace) + { } + + /// + /// Gets or sets the that the argument's parsed tokens will be converted to. + /// + public abstract Type ValueType { get; } + + internal bool IsBoolean() => ValueType == typeof(bool) || ValueType == typeof(bool?); + +} diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index cdf9767036..f59c464286 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -10,23 +10,25 @@ namespace System.CommandLine /// /// A symbol defining a named parameter and a value for that parameter. /// - public abstract class CliOption : CliSymbol + public abstract class CliOption : CliDataSymbol { - // TODO: don't expose field internal AliasSet? _aliases; -/* + /* private List>? _validators; */ - private protected CliOption(string name, string[] aliases) : base(name) + private protected CliOption(string name, string[] aliases) + : base(name) { - if (aliases is { Length: > 0 }) + if (aliases is { Length: > 0 }) { _aliases = new(aliases); } } + public override Type ValueType => Argument.ValueType; + /// /// Gets the argument for the option. /// @@ -37,8 +39,8 @@ private protected CliOption(string name, string[] aliases) : base(name) /// public bool HasDefaultValue => Argument.HasDefaultValue; -// TODO: help -/* + // TODO: help + /* /// /// Gets or sets the name of the Option when displayed in help. /// @@ -51,7 +53,7 @@ public string? HelpName get => Argument.HelpName; set => Argument.HelpName = value; } -*/ + */ /// /// Gets or sets the arity of the option. @@ -62,8 +64,8 @@ public ArgumentArity Arity set => Argument.Arity = value; } -// TODO: recursive options, validators, completion -/* + // TODO: recursive options, validators, completion + /* /// /// When set to true, this option will be applied to its immediate parent command or commands and recursively to their subcommands. /// @@ -80,9 +82,9 @@ public ArgumentArity Arity /// Gets the list of completion sources for the option. /// public List>> CompletionSources => Argument.CompletionSources; -*/ + */ -// TODO: what does this even mean? + // TODO: what does this even mean? /// /// Gets a value that indicates whether multiple argument tokens are allowed for each option identifier token. /// @@ -98,10 +100,10 @@ public ArgumentArity Arity /// public bool AllowMultipleArgumentsPerToken { get; set; } -// TODO: rename to IsGreedy + // TODO: rename to IsGreedy internal virtual bool Greedy => Argument.Arity.MinimumNumberOfValues > 0 && Argument.ValueType != typeof(bool); - -// TODO: rename to IsRequired + + // TODO: rename to IsRequired and move to Validation /// /// Indicates whether the option is required when its parent command is invoked. /// @@ -114,8 +116,8 @@ public ArgumentArity Arity /// The collection does not contain the of the Option. public ICollection Aliases => _aliases ??= new(); -// TODO: invocation, completion -/* + // TODO: invocation, completion + /* /// /// Gets or sets the for the Option. The handler represents the action /// that will be performed when the Option is invoked. diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 9e45744e6b..9387bf4bab 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -27,6 +27,7 @@ + From 5c656bcb5b9fa8b7e0ca5328241c3d2186cbcb3e Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Fri, 9 Aug 2024 21:39:53 -0400 Subject: [PATCH 109/150] Remove type from annotation ID The purpose of this was to ensure that accessors used the correct type for the annotation values. This originally applied to the AnnotationAccessor helper for symbol.With(help.Description, desc) but also applied to later manually defined accessors. This strong typing was not perfect, for example it was possible to (incorrectly) define two annotations with the same ID and different types, resulting in uninformative cast exceptions. It also did not apply cleanly to more advanced cases, such as when the value was a Func or where the annotation value type was based on the symbol value type. It is also not unreasonable to want to store more than one type in an annotataion for othe reasons. All these required removing the typing by using AnnotationId. This miakes things a bit cleaner by removing the strong typing from the annotation ID and instead adding TryGetAnnotation overloads that take a type and throw an informative exception on mismatch. This only affects folks defining new annotations. It does not change anything for authors of CLIs or authors of subsystems, as they should to use the strongly typed accessors that should be defined along with every annotation ID. --- .../HelpAnnotationExtensions.cs | 2 +- .../Subsystems/Annotations/AnnotationId.cs | 2 +- ...tionStorageExtensions.AnnotationStorage.cs | 17 ++--- .../AnnotationStorageExtensions.cs | 72 +++++++++++++++---- .../Annotations/AnnotationTypeException.cs | 33 +++++++++ .../Subsystems/Annotations/HelpAnnotations.cs | 5 +- .../Annotations/ValueAnnotations.cs | 16 +++-- .../Subsystems/CliSubsystem.cs | 44 +++++++++++- .../Subsystems/IAnnotationProvider.cs | 2 +- .../ValueAnnotationExtensions.cs | 14 ++-- .../ValueSubsystem.cs | 6 +- 11 files changed, 163 insertions(+), 50 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs diff --git a/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs index 57e9c771f1..8cb06c594d 100644 --- a/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs @@ -45,6 +45,6 @@ public static void SetDescription(this TSymbol symbol, string descripti /// public static string? GetDescription(this TSymbol symbol) where TSymbol : CliSymbol { - return symbol.GetAnnotationOrDefault(HelpAnnotations.Description); + return symbol.GetAnnotationOrDefault(HelpAnnotations.Description); } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationId.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationId.cs index 28928e90e1..bda76cf6cb 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationId.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationId.cs @@ -6,7 +6,7 @@ namespace System.CommandLine.Subsystems.Annotations; /// /// Describes the ID and type of an annotation. /// -public record struct AnnotationId(string Prefix, string Id) +public record struct AnnotationId(string Prefix, string Id) { public override readonly string ToString() => $"{Prefix}.{Id}"; } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs index 5123f11d20..58341d3bf1 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs @@ -11,25 +11,16 @@ class AnnotationStorage : IAnnotationProvider { record struct AnnotationKey(CliSymbol symbol, string prefix, string id) { - public static AnnotationKey Create (CliSymbol symbol, AnnotationId annotationId) + public static AnnotationKey Create (CliSymbol symbol, AnnotationId annotationId) => new (symbol, annotationId.Prefix, annotationId.Id); } readonly Dictionary annotations = []; - public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) - { - if (annotations.TryGetValue(AnnotationKey.Create(symbol, annotationId), out var obj)) - { - value = (TValue)obj; - return true; - } - - value = default; - return false; - } + public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out object? value) + => annotations.TryGetValue(AnnotationKey.Create(symbol, annotationId), out value); - public void Set(CliSymbol symbol, AnnotationId annotationId, TValue value) + public void Set(CliSymbol symbol, AnnotationId annotationId, object? value) { var key = AnnotationKey.Create(symbol, annotationId); if (value is not null) diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs index fff6373677..5a8a4be569 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs @@ -57,13 +57,12 @@ public static partial class AnnotationStorageExtensions /// /// Sets the value for the annotation associated with the in the internal annotation storage. /// - /// The type of the annotation value /// The symbol that is annotated /// /// The identifier for the annotation. For example, the annotation identifier for the help description is . /// /// The annotation value - public static void SetAnnotation(this CliSymbol symbol, AnnotationId annotationId, TValue value) + public static void SetAnnotation(this CliSymbol symbol, AnnotationId annotationId, object? value) { var storage = symbolToAnnotationStorage.GetValue(symbol, static (CliSymbol _) => new AnnotationStorage()); storage.Set(symbol, annotationId, value); @@ -73,7 +72,6 @@ public static void SetAnnotation(this CliSymbol symbol, AnnotationId associated with the in the internal annotation storage, /// and returns the to enable fluent construction of symbols with annotations. /// - /// The type of the annotation value /// The symbol that is annotated /// /// The identifier for the annotation. For example, the annotation identifier for the help description is . @@ -82,7 +80,7 @@ public static void SetAnnotation(this CliSymbol symbol, AnnotationId /// The , to enable fluent construction of symbols with annotations. /// - public static TSymbol WithAnnotation(this TSymbol symbol, AnnotationId annotationId, TValue value) where TSymbol : CliSymbol + public static TSymbol WithAnnotation(this TSymbol symbol, AnnotationId annotationId, object? value) where TSymbol : CliSymbol { symbol.SetAnnotation(annotationId, value); return symbol; @@ -90,9 +88,13 @@ public static TSymbol WithAnnotation(this TSymbol symbol, Annot /// /// Attempts to get the value for the annotation associated with the in the internal annotation - /// storage used to store values set via . + /// storage used to store values set via . /// - /// The type of the annotation value + /// + /// The expected type of the annotation value. If the type does not match, a will be thrown. + /// If the annotation allows multiple types for its values, and a type parameter cannot be determined statically, + /// use to access the annotation value without checking its type. + /// /// The symbol that is annotated /// /// The identifier for the annotation. For example, the annotation identifier for the help description is . @@ -100,14 +102,56 @@ public static TSymbol WithAnnotation(this TSymbol symbol, Annot /// The annotation value, if successful, otherwise default /// True if successful /// - /// This is intended to be called by specialized ID-specific accessors for CLI authors such as . - /// Subsystems should not call it, as it does not account for values from the subsystem's . They should instead call - /// or an ID-specific accessor on the subsystem such + /// If the annotation value does not have a single expected type for this symbol, use the overload instead. + /// + /// This is intended to be called by the implementation of specialized ID-specific accessors for CLI authors such as . + /// + /// + /// Subsystems should not call it directly, as it does not account for values from the subsystem's . They should instead call + /// or an ID-specific accessor on the subsystem such + /// . + /// + /// + public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) + { + if (TryGetAnnotation(symbol, annotationId, out object? rawValue)) + { + if (rawValue is TValue expectedTypeValue) + { + value = expectedTypeValue; + return true; + } + throw new AnnotationTypeException(annotationId, typeof(TValue), rawValue?.GetType()); + } + + value = default; + return false; + } + + /// + /// Attempts to get the value for the annotation associated with the in the internal annotation + /// storage used to store values set via . + /// + /// The symbol that is annotated + /// + /// The identifier for the annotation. For example, the annotation identifier for the help description is . + /// + /// The annotation value, if successful, otherwise default + /// True if successful + /// + /// If the expected type of the annotation value is known, use the overload instead. + /// + /// This is intended to be called by the implementation of specialized ID-specific accessors for CLI authors such as . + /// + /// + /// Subsystems should not call it directly, as it does not account for values from the subsystem's . They should instead call + /// or an ID-specific accessor on the subsystem such /// . + /// /// - public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) + public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out object? value) { - if (symbolToAnnotationStorage.TryGetValue(symbol, out var storage) && storage.TryGet (symbol, annotationId, out value)) + if (symbolToAnnotationStorage.TryGetValue(symbol, out var storage) && storage.TryGet(symbol, annotationId, out value)) { return true; } @@ -118,7 +162,7 @@ public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId< /// /// Attempts to get the value for the annotation associated with the in the internal annotation - /// storage used to store values set via . + /// storage used to store values set via . /// /// The type of the annotation value /// The symbol that is annotated @@ -129,10 +173,10 @@ public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId< /// /// This is intended to be called by specialized ID-specific accessors for CLI authors such as . /// Subsystems should not call it, as it does not account for values from the subsystem's . They should instead call - /// or an ID-specific accessor on the subsystem such + /// or an ID-specific accessor on the subsystem such /// . /// - public static TValue? GetAnnotationOrDefault(this CliSymbol symbol, AnnotationId annotationId) + public static TValue? GetAnnotationOrDefault(this CliSymbol symbol, AnnotationId annotationId) { if (symbol.TryGetAnnotation(annotationId, out TValue? value)) { diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs new file mode 100644 index 0000000000..4d1a19aded --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs @@ -0,0 +1,33 @@ +// 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.Subsystems.Annotations; + +/// +/// Thrown when an annotation value does not match the expected type for that . +/// +public class AnnotationTypeException(AnnotationId annotationId, Type expectedType, Type? actualType, IAnnotationProvider? provider = null) : Exception +{ + public AnnotationId AnnotationId { get; } = annotationId; + public Type ExpectedType { get; } = expectedType; + public Type? ActualType { get; } = actualType; + public IAnnotationProvider? Provider = provider; + + public override string Message + { + get + { + if (Provider is not null) + { + return + $"Typed accessor for annotation '${AnnotationId}' expected type '{ExpectedType}' but the annotation provider returned an annotation of type '{ActualType?.ToString() ?? "[null]"}'. " + + $"This may be an authoring error in in the annotation provider '{Provider.GetType()}' or in a typed annotation accessor."; + + } + + return + $"Typed accessor for annotation '${AnnotationId}' expected type '{ExpectedType}' but the stored annotation is of type '{ActualType?.ToString() ?? "[null]"}'. " + + $"This may be an authoring error in a typed annotation accessor, or the annotation may have be stored directly with the incorrect type, bypassing the typed accessors."; + } + } +} \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs index 2dba0ab830..fb0ced77a7 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/HelpAnnotations.cs @@ -10,5 +10,8 @@ public static class HelpAnnotations { public static string Prefix { get; } = nameof(SubsystemKind.Help); - public static AnnotationId Description { get; } = new(Prefix, nameof(Description)); + /// + /// The description of the symbol, as a plain text . + /// + public static AnnotationId Description { get; } = new(Prefix, nameof(Description)); } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs index 466f34f2b4..8212797714 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs @@ -14,18 +14,20 @@ public static class ValueAnnotations /// Default value for an option or argument /// /// - /// Although the type is , it must actually be the same type as the type - /// parameter of the or . + /// Must be actually the same type as the type parameter of + /// the or . /// - public static AnnotationId DefaultValue { get; } = new(Prefix, nameof(DefaultValue)); + public static AnnotationId DefaultValue { get; } = new(Prefix, nameof(DefaultValue)); /// /// Default value calculation for an option or argument /// /// - /// Although the type is , it must actually be a - /// with a type parameter matching the the type parameter type of the - /// or + /// Please use the extension methods and do not call this directly. + /// + /// Must return a with the same type parameter as + /// the or . + /// /// - public static AnnotationId DefaultValueCalculation { get; } = new(Prefix, nameof(DefaultValueCalculation)); + public static AnnotationId DefaultValueCalculation { get; } = new(Prefix, nameof(DefaultValueCalculation)); } diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index f48a27a015..703716b5b6 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -36,7 +36,11 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv /// Attempt to retrieve the 's value for the annotation . This will check the /// annotation provider that was passed to the subsystem constructor, and the internal annotation storage. /// - /// The value of the type to retrieve + /// + /// The expected type of the annotation value. If the type does not match, a will be thrown. + /// If the annotation allows multiple types for its values, and a type parameter cannot be determined statically, + /// use to access the annotation value without checking its type. + /// /// The symbol the value is attached to /// /// The identifier for the annotation value to be retrieved. @@ -45,10 +49,46 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv /// An out parameter to contain the result /// True if successful /// + /// If the annotation value does not have a single expected type for this symbol, use the overload instead. + /// /// Subsystem authors must use this to access annotation values, as it respects the subsystem's if it has one. /// This value is protected because it is intended for use only by subsystem authors. It calls + /// /// - protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) + protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) + { + if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, annotationId, out object? rawValue)) + { + if (rawValue is TValue expectedTypeValue) + { + value = expectedTypeValue; + return true; + } + throw new AnnotationTypeException(annotationId, typeof(TValue), rawValue?.GetType(), _annotationProvider); + } + + return symbol.TryGetAnnotation(annotationId, out value); + } + + /// + /// Attempt to retrieve the 's value for the annotation . This will check the + /// annotation provider that was passed to the subsystem constructor, and the internal annotation storage. + /// + /// The symbol the value is attached to + /// + /// The identifier for the annotation value to be retrieved. + /// For example, the annotation identifier for the help description is . + /// + /// An out parameter to contain the result + /// True if successful + /// + /// If the expected type of the annotation value is known, use the overload instead. + /// + /// Subsystem authors must use this to access annotation values, as it respects the subsystem's if it has one. + /// This value is protected because it is intended for use only by subsystem authors. It calls + /// + /// + protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out object? value) { if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, annotationId, out value)) { diff --git a/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs b/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs index 8b3c4203ac..dd4d9e4fd5 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs @@ -11,5 +11,5 @@ namespace System.CommandLine.Subsystems; /// public interface IAnnotationProvider { - bool TryGet(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out TValue? value); + bool TryGet(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out object? value); } diff --git a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs index 2e5b12c277..137b1f8982 100644 --- a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs @@ -45,9 +45,9 @@ public static void SetDefaultValue(this CliOption option, TValue /// public static TValue? GetDefaultValueAnnotation(this CliOption option) { - if (option.TryGetAnnotation(ValueAnnotations.DefaultValue, out var defaultValue)) + if (option.TryGetAnnotation(ValueAnnotations.DefaultValue, out TValue? defaultValue)) { - return (TValue?)defaultValue; + return defaultValue; } return default; } @@ -90,7 +90,7 @@ public static void SetDefaultValue(this CliArgument argument, TV /// public static TValue? GetDefaultValueAnnotation(this CliArgument argument) { - if (argument.TryGetAnnotation(ValueAnnotations.DefaultValue, out var defaultValue)) + if (argument.TryGetAnnotation(ValueAnnotations.DefaultValue, out TValue? defaultValue)) { return (TValue?)defaultValue; } @@ -134,9 +134,9 @@ public static void SetDefaultValueCalculation(this CliOption opt /// public static Func? GetDefaultValueCalculation(this CliOption option) { - if (option.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out var defaultValueCalculation)) + if (option.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation)) { - return (Func)defaultValueCalculation; + return defaultValueCalculation; } return default; } @@ -179,9 +179,9 @@ public static void SetDefaultValueCalculation(this CliArgument a /// public static Func? GetDefaultValueCalculation(this CliArgument argument) { - if (argument.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out var defaultValueCalculation)) + if (argument.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation)) { - return (Func)defaultValueCalculation; + return defaultValueCalculation; } return default; } diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueSubsystem.cs index 0ff3abb255..f0c334239b 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueSubsystem.cs @@ -74,10 +74,10 @@ not null when TryGetValue(symbol, out var value) // configuration values go here in precedence //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) // => UseValue(symbol, GetEnvByName(envName)), - not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValueCalculation, out var defaultValueCalculation) + not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation) => UseValue(symbol, CalculatedDefault(symbol, (Func)defaultValueCalculation)), - not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var explicitValue) - => UseValue(symbol, (T)explicitValue), + not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out T? explicitValue) + => UseValue(symbol, explicitValue), null => throw new ArgumentNullException(nameof(symbol)), _ => UseValue(symbol, default(T)) }; From 726655e48912856c96f7b44db71aae92a07409c9 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Tue, 13 Aug 2024 09:50:52 -0700 Subject: [PATCH 110/150] Rename to ValueSymbol --- src/System.CommandLine/CliArgument.cs | 3 +-- src/System.CommandLine/CliOption.cs | 2 +- .../{CliDataSymbol.cs => CliValueSymbol.cs} | 4 ++-- src/System.CommandLine/System.CommandLine.csproj | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) rename src/System.CommandLine/{CliDataSymbol.cs => CliValueSymbol.cs} (81%) diff --git a/src/System.CommandLine/CliArgument.cs b/src/System.CommandLine/CliArgument.cs index 3e28f7ce42..89c83d6698 100644 --- a/src/System.CommandLine/CliArgument.cs +++ b/src/System.CommandLine/CliArgument.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.CommandLine.Parsing; @@ -10,7 +9,7 @@ namespace System.CommandLine /// /// A symbol defining a value that can be passed on the command line to a command or option. /// - public abstract class CliArgument : CliDataSymbol + public abstract class CliArgument : CliValueSymbol { private ArgumentArity _arity; // TODO: custom parser, completion, validators diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index f59c464286..ae0a9b7ebf 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -10,7 +10,7 @@ namespace System.CommandLine /// /// A symbol defining a named parameter and a value for that parameter. /// - public abstract class CliOption : CliDataSymbol + public abstract class CliOption : CliValueSymbol { internal AliasSet? _aliases; /* diff --git a/src/System.CommandLine/CliDataSymbol.cs b/src/System.CommandLine/CliValueSymbol.cs similarity index 81% rename from src/System.CommandLine/CliDataSymbol.cs rename to src/System.CommandLine/CliValueSymbol.cs index 8aa9069e34..812961b75a 100644 --- a/src/System.CommandLine/CliDataSymbol.cs +++ b/src/System.CommandLine/CliValueSymbol.cs @@ -3,9 +3,9 @@ namespace System.CommandLine; -public abstract class CliDataSymbol : CliSymbol +public abstract class CliValueSymbol : CliSymbol { - protected CliDataSymbol(string name, bool allowWhitespace = false) + protected CliValueSymbol(string name, bool allowWhitespace = false) : base(name, allowWhitespace) { } diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 9387bf4bab..85e34a8975 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -27,7 +27,7 @@ - + From 190867b33b0f52757f9a8104afb8d11c86724343 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Tue, 13 Aug 2024 16:20:50 -0400 Subject: [PATCH 111/150] Renamed result files and created CliSymbolResult base CommandResult -> CliCommandResultInternal OptionResult -> CliOptionResultInternal ArgumentResult -> CliArgumentResultInternal SymbolResult -> CliSymbolResultInternal ValueResult -> CliValueResult CommandValueResult -> CliCommandResult Created CliSymbolResult as abase class for ValueResult and CommandValueResult --- .../Help/HelpOptionAction.cs | 2 +- .../Invocation/ParseErrorAction.cs | 6 +- .../VersionOption.cs | 2 +- .../BindingContextExtensions.cs | 6 +- .../CommandResultExtensions.cs | 4 +- .../ParseResultMatchingValueSource.cs | 4 +- .../Directives/DiagramSubsystem.cs | 8 +- src/System.CommandLine.Tests/CommandTests.cs | 24 ++-- .../CustomParsingTests.cs | 18 +-- .../ParseResultTests.cs | 4 +- .../ParserTests.MultiplePositions.cs | 4 +- src/System.CommandLine.Tests/ParserTests.cs | 108 +++++++++--------- .../ParsingValidationTests.cs | 56 ++++----- .../ResponseFileTests.cs | 8 +- src/System.CommandLine/ArgumentArity.cs | 6 +- .../Binding/ArgumentConversionResult.cs | 24 ++-- .../Binding/ArgumentConverter.cs | 22 ++-- .../Binding/TryConvertArgument.cs | 2 +- src/System.CommandLine/CliArgument.cs | 4 +- src/System.CommandLine/CliArgument{T}.cs | 4 +- src/System.CommandLine/CliCommand.cs | 4 +- src/System.CommandLine/CliOption{T}.cs | 2 +- .../LocalizationResources.cs | 12 +- src/System.CommandLine/ParseResult.cs | 48 ++++---- ...Result.cs => CliArgumentResultInternal.cs} | 36 +++--- ...mandValueResult.cs => CliCommandResult.cs} | 10 +- ...dResult.cs => CliCommandResultInternal.cs} | 39 ++++--- ...onResult.cs => CliOptionResultInternal.cs} | 16 +-- .../Parsing/CliSymbolResult.cs | 9 ++ ...olResult.cs => CliSymbolResultInternal.cs} | 18 +-- .../{ValueResult.cs => CliValueResult.cs} | 10 +- .../Parsing/DirectiveResult.cs | 2 +- .../Parsing/ParseDiagramAction.cs | 8 +- src/System.CommandLine/Parsing/ParseError.cs | 8 +- .../Parsing/ParseOperation.cs | 34 +++--- .../Parsing/SymbolLookupByName.cs | 6 +- .../Parsing/SymbolResultExtensions.cs | 4 +- .../Parsing/SymbolResultTree.cs | 32 +++--- .../System.CommandLine.csproj | 13 ++- 39 files changed, 319 insertions(+), 308 deletions(-) rename src/System.CommandLine/Parsing/{ArgumentResult.cs => CliArgumentResultInternal.cs} (86%) rename src/System.CommandLine/Parsing/{CommandValueResult.cs => CliCommandResult.cs} (72%) rename src/System.CommandLine/Parsing/{CommandResult.cs => CliCommandResultInternal.cs} (79%) rename src/System.CommandLine/Parsing/{OptionResult.cs => CliOptionResultInternal.cs} (83%) create mode 100644 src/System.CommandLine/Parsing/CliSymbolResult.cs rename src/System.CommandLine/Parsing/{SymbolResult.cs => CliSymbolResultInternal.cs} (88%) rename src/System.CommandLine/Parsing/{ValueResult.cs => CliValueResult.cs} (95%) diff --git a/src/System.CommandLine.Extended/Help/HelpOptionAction.cs b/src/System.CommandLine.Extended/Help/HelpOptionAction.cs index 9700643ae5..fe034647fd 100644 --- a/src/System.CommandLine.Extended/Help/HelpOptionAction.cs +++ b/src/System.CommandLine.Extended/Help/HelpOptionAction.cs @@ -24,7 +24,7 @@ public override int Invoke(ParseResult parseResult) var output = parseResult.Configuration.Output; var helpContext = new HelpContext(Builder, - parseResult.CommandResult.Command, + parseResult.CommandResultInternal.Command, output, parseResult); diff --git a/src/System.CommandLine.Extended/Invocation/ParseErrorAction.cs b/src/System.CommandLine.Extended/Invocation/ParseErrorAction.cs index 5175ad70d7..c85e432757 100644 --- a/src/System.CommandLine.Extended/Invocation/ParseErrorAction.cs +++ b/src/System.CommandLine.Extended/Invocation/ParseErrorAction.cs @@ -66,8 +66,8 @@ private static void WriteHelp(ParseResult parseResult) // Find the most proximate help option (if any) and invoke its action. var availableHelpOptions = parseResult - .CommandResult - .RecurseWhileNotNull(r => r.Parent as CommandResult) + .CommandResultInternal + .RecurseWhileNotNull(r => r.Parent as CommandResultInternal) .Select(r => r.Command.Options.OfType().FirstOrDefault()); if (availableHelpOptions.FirstOrDefault(o => o is not null) is { Action: not null } helpOption) { @@ -92,7 +92,7 @@ private static void WriteTypoCorrectionSuggestions(ParseResult parseResult) var token = unmatchedTokens[i]; bool first = true; - foreach (string suggestion in GetPossibleTokens(parseResult.CommandResult.Command, token)) + foreach (string suggestion in GetPossibleTokens(parseResult.CommandResultInternal.Command, token)) { if (first) { diff --git a/src/System.CommandLine.Extended/VersionOption.cs b/src/System.CommandLine.Extended/VersionOption.cs index 48f56dbc77..53be33155d 100644 --- a/src/System.CommandLine.Extended/VersionOption.cs +++ b/src/System.CommandLine.Extended/VersionOption.cs @@ -47,7 +47,7 @@ private void AddValidators() { Validators.Add(static result => { - if (result.Parent is CommandResult parent && + if (result.Parent is CliCommandResultInternal parent && parent.Children.Any(r => r is not OptionResult { Option: VersionOption })) { result.AddError(LocalizationResources.VersionOptionCannotBeCombinedWithOtherArguments(result.IdentifierToken?.Value ?? result.Option.Name)); diff --git a/src/System.CommandLine.NamingConventionBinder/BindingContextExtensions.cs b/src/System.CommandLine.NamingConventionBinder/BindingContextExtensions.cs index fe707c2347..d1e9ea54a4 100644 --- a/src/System.CommandLine.NamingConventionBinder/BindingContextExtensions.cs +++ b/src/System.CommandLine.NamingConventionBinder/BindingContextExtensions.cs @@ -20,12 +20,12 @@ private sealed class DummyStateHoldingHandler : BindingHandler public static BindingContext GetBindingContext(this ParseResult parseResult) { // parsing resulted with no handler or it was not created yet, we fake it to just store the BindingContext between the calls - if (parseResult.CommandResult.Command.Action is null) + if (parseResult.CommandResultInternal.Command.Action is null) { - parseResult.CommandResult.Command.Action = new DummyStateHoldingHandler(); + parseResult.CommandResultInternal.Command.Action = new DummyStateHoldingHandler(); } - return ((BindingHandler)parseResult.CommandResult.Command.Action).GetBindingContext(parseResult); + return ((BindingHandler)parseResult.CommandResultInternal.Command.Action).GetBindingContext(parseResult); } /// diff --git a/src/System.CommandLine.NamingConventionBinder/CommandResultExtensions.cs b/src/System.CommandLine.NamingConventionBinder/CommandResultExtensions.cs index f20269245e..223056ac29 100644 --- a/src/System.CommandLine.NamingConventionBinder/CommandResultExtensions.cs +++ b/src/System.CommandLine.NamingConventionBinder/CommandResultExtensions.cs @@ -8,7 +8,7 @@ namespace System.CommandLine.NamingConventionBinder; internal static class CommandResultExtensions { internal static bool TryGetValueForArgument( - this CommandResult commandResult, + this CliCommandResultInternal commandResult, IValueDescriptor valueDescriptor, out object? value) { @@ -38,7 +38,7 @@ internal static bool TryGetValueForArgument( } internal static bool TryGetValueForOption( - this CommandResult commandResult, + this CliCommandResultInternal commandResult, IValueDescriptor valueDescriptor, out object? value) { diff --git a/src/System.CommandLine.NamingConventionBinder/ParseResultMatchingValueSource.cs b/src/System.CommandLine.NamingConventionBinder/ParseResultMatchingValueSource.cs index 4168c99deb..190b85318b 100644 --- a/src/System.CommandLine.NamingConventionBinder/ParseResultMatchingValueSource.cs +++ b/src/System.CommandLine.NamingConventionBinder/ParseResultMatchingValueSource.cs @@ -15,7 +15,7 @@ public bool TryGetValue( { if (!string.IsNullOrEmpty(valueDescriptor.ValueName)) { - CommandResult? commandResult = bindingContext?.ParseResult.CommandResult; + CliCommandResultInternal? commandResult = bindingContext?.ParseResult.CommandResultInternal; while (commandResult is { }) { @@ -34,7 +34,7 @@ public bool TryGetValue( return true; } - commandResult = commandResult.Parent as CommandResult; + commandResult = commandResult.Parent as CliCommandResultInternal; } } diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 104d0c3f07..f05627aa1f 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -60,10 +60,10 @@ internal static StringBuilder Diagram(ParseResult parseResult) /* private static void Diagram( StringBuilder builder, - SymbolResult symbolResult, + CliSymbolResultInternal symbolResult, ParseResult parseResult) { - if (parseResult.Errors.Any(e => e.SymbolResult == symbolResult)) + if (parseResult.Errors.Any(e => e.SymbolResultInternal == symbolResult)) { builder.Append('!'); } @@ -155,10 +155,10 @@ private static void Diagram( } else { - builder.Append(((CommandResult)symbolResult).IdentifierToken.Value); + builder.Append(((CliCommandResultInternal)symbolResult).IdentifierToken.Value); } - foreach (SymbolResult child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) + foreach (CliSymbolResultInternal child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) { if (child is ArgumentResult arg && (arg.Argument.ValueType == typeof(bool) || diff --git a/src/System.CommandLine.Tests/CommandTests.cs b/src/System.CommandLine.Tests/CommandTests.cs index 8e2157932d..628068d7ce 100644 --- a/src/System.CommandLine.Tests/CommandTests.cs +++ b/src/System.CommandLine.Tests/CommandTests.cs @@ -42,10 +42,10 @@ public void Outer_command_is_identified_correctly_by_Parent_property() var result = _outerCommand.Parse("outer inner --option argument1"); result - .CommandResult + .CommandResultInternal .Parent .Should() - .BeOfType() + .BeOfType() .Which .Command .Name @@ -58,9 +58,9 @@ public void Inner_command_is_identified_correctly() { var result = _outerCommand.Parse("outer inner --option argument1"); - result.CommandResult + result.CommandResultInternal .Should() - .BeOfType() + .BeOfType() .Which .Command .Name @@ -73,7 +73,7 @@ public void Inner_command_option_is_identified_correctly() { var result = _outerCommand.Parse("outer inner --option argument1"); - result.CommandResult + result.CommandResultInternal .Children .ElementAt(0) .Should() @@ -90,7 +90,7 @@ public void Inner_command_option_argument_is_identified_correctly() { var result = _outerCommand.Parse("outer inner --option argument1"); - result.CommandResult + result.CommandResultInternal .Children .ElementAt(0) .Tokens @@ -114,14 +114,14 @@ public void Commands_at_multiple_levels_can_have_their_own_arguments() var result = outer.Parse("outer arg1 inner arg2 arg3"); - result.CommandResult + result.CommandResultInternal .Parent .Tokens .Select(t => t.Value) .Should() .BeEquivalentTo("arg1"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -200,7 +200,7 @@ public void ParseResult_Command_identifies_innermost_command(string input, strin var result = outer.Parse(input); - result.CommandResult.Command.Name.Should().Be(expectedCommand); + result.CommandResultInternal.Command.Name.Should().Be(expectedCommand); } [Fact] @@ -214,7 +214,7 @@ public void Commands_can_have_aliases() var result = command.Parse("that"); - result.CommandResult.Command.Should().BeSameAs(command); + result.CommandResultInternal.Command.Should().BeSameAs(command); result.Errors.Should().BeEmpty(); } @@ -228,7 +228,7 @@ public void RootCommand_can_have_aliases() var result = command.Parse("that"); - result.CommandResult.Command.Should().BeSameAs(command); + result.CommandResultInternal.Command.Should().BeSameAs(command); result.Errors.Should().BeEmpty(); } @@ -245,7 +245,7 @@ public void Subcommands_can_have_aliases() var result = rootCommand.Parse("that"); - result.CommandResult.Command.Should().BeSameAs(subcommand); + result.CommandResultInternal.Command.Should().BeSameAs(subcommand); result.Errors.Should().BeEmpty(); } diff --git a/src/System.CommandLine.Tests/CustomParsingTests.cs b/src/System.CommandLine.Tests/CustomParsingTests.cs index e29c5ba32e..b1fe4223e1 100644 --- a/src/System.CommandLine.Tests/CustomParsingTests.cs +++ b/src/System.CommandLine.Tests/CustomParsingTests.cs @@ -95,7 +95,7 @@ public void Validation_failure_message_can_be_specified_when_parsing_tokens() new CliRootCommand { argument }.Parse("x") .Errors .Should() - .ContainSingle(e => ((ArgumentResult)e.SymbolResult).Argument == argument) + .ContainSingle(e => ((ArgumentResult)e.SymbolResultInternal).Argument == argument) .Which .Message .Should() @@ -117,7 +117,7 @@ public void Validation_failure_message_can_be_specified_when_evaluating_default_ new CliRootCommand { argument }.Parse("") .Errors .Should() - .ContainSingle(e => ((ArgumentResult)e.SymbolResult).Argument == argument) + .ContainSingle(e => ((ArgumentResult)e.SymbolResultInternal).Argument == argument) .Which .Message .Should() @@ -256,7 +256,7 @@ public void Option_ArgumentResult_parentage_to_root_symbol_is_set_correctly_when .Parent .Parent .Should() - .BeOfType() + .BeOfType() .Which .Command .Should() @@ -268,7 +268,7 @@ public void Option_ArgumentResult_parentage_to_root_symbol_is_set_correctly_when [InlineData("-y value-y -x value-x")] public void Symbol_can_be_found_without_explicitly_traversing_result_tree(string commandLine) { - SymbolResult resultForOptionX = null; + CliSymbolResultInternal resultForOptionX = null; var optionX = new CliOption("-x") { CustomParser = _ => string.Empty @@ -322,7 +322,7 @@ public void Command_ArgumentResult_Parent_is_set_correctly_when_token_is_implici argumentResult .Parent .Should() - .BeOfType() + .BeOfType() .Which .Command .Should() @@ -447,7 +447,7 @@ public void Custom_parser_can_check_another_option_result_for_custom_errors(stri var parseResult = command.Parse(commandLine); parseResult.Errors - .Single(e => e.SymbolResult is OptionResult optResult && + .Single(e => e.SymbolResultInternal is OptionResult optResult && optResult.Option == optionThatDependsOnOptionWithError) .Message .Should() @@ -482,8 +482,8 @@ public void Validation_reports_all_parse_errors() OptionResult secondOptionResult = parseResult.GetResult(secondOptionWithError); secondOptionResult.Errors.Single().Message.Should().Be("second error"); - parseResult.Errors.Should().Contain(error => error.SymbolResult == firstOptionResult); - parseResult.Errors.Should().Contain(error => error.SymbolResult == secondOptionResult); + parseResult.Errors.Should().Contain(error => error.SymbolResultInternal == firstOptionResult); + parseResult.Errors.Should().Contain(error => error.SymbolResultInternal == secondOptionResult); } [Fact] @@ -504,7 +504,7 @@ public void When_custom_conversion_fails_then_an_option_does_not_accept_further_ var result = command.Parse("the-command -x nope yep"); - result.CommandResult.Tokens.Count.Should().Be(1); + result.CommandResultInternal.Tokens.Count.Should().Be(1); } [Fact] diff --git a/src/System.CommandLine.Tests/ParseResultTests.cs b/src/System.CommandLine.Tests/ParseResultTests.cs index b8f9948e93..207e6d732d 100644 --- a/src/System.CommandLine.Tests/ParseResultTests.cs +++ b/src/System.CommandLine.Tests/ParseResultTests.cs @@ -94,12 +94,12 @@ public void Command_will_not_accept_a_command_if_a_sibling_command_has_already_b var result = CliParser.Parse(command, "outer inner-one inner-two"); - result.CommandResult.Command.Name.Should().Be("inner-one"); + result.CommandResultInternal.Command.Name.Should().Be("inner-one"); result.Errors.Count.Should().Be(1); var result2 = CliParser.Parse(command, "outer inner-two inner-one"); - result2.CommandResult.Command.Name.Should().Be("inner-two"); + result2.CommandResultInternal.Command.Name.Should().Be("inner-two"); result2.Errors.Count.Should().Be(1); } diff --git a/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs b/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs index 7c74880137..8480bb967f 100644 --- a/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs +++ b/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs @@ -136,10 +136,10 @@ public void A_command_can_be_specified_in_more_than_one_position( var result = outer.Parse(commandLine); result.Errors.Should().BeEmpty(); - result.CommandResult + result.CommandResultInternal .Parent .Should() - .BeOfType() + .BeOfType() .Which .Command .Name diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 899552c804..fb4391a2db 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -139,9 +139,9 @@ public void Option_short_forms_can_be_bundled() var result = CliParser.Parse(command, "the-command -xyz"); - result.CommandResult + result.CommandResultInternal .Children - .Select(o => ((OptionResult)o).Option.Name) + .Select(o => ((CliOptionResultInternal)o).Option.Name) .Should() .BeEquivalentTo("-x", "-y", "-z"); } @@ -189,9 +189,9 @@ public void Option_long_forms_do_not_get_unbundled() var result = CliParser.Parse(command, "the-command --xyz"); - result.CommandResult + result.CommandResultInternal .Children - .Select(o => ((OptionResult)o).Option.Name) + .Select(o => ((CliOptionResultInternal)o).Option.Name) .Should() .BeEquivalentTo("--xyz"); } @@ -211,7 +211,7 @@ public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_v ParseResult result = CliParser.Parse(outer, "outer inner -abc"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -425,7 +425,7 @@ public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_foll .Should() .BeEquivalentTo("carrot"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -443,17 +443,17 @@ public void Command_with_multiple_options_is_parsed_correctly() var result = CliParser.Parse(command, "outer --inner1 argument1 --inner2 argument2"); - result.CommandResult + result.CommandResultInternal .Children .Should() .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner1" && + ((CliOptionResultInternal)o).Option.Name == "--inner1" && o.Tokens.Single().Value == "argument1"); - result.CommandResult + result.CommandResultInternal .Children .Should() .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner2" && + ((CliOptionResultInternal)o).Option.Name == "--inner2" && o.Tokens.Single().Value == "argument2"); } @@ -576,13 +576,13 @@ public void When_nested_commands_all_accept_arguments_then_the_nearest_captures_ var result = CliParser.Parse(command, "outer arg1 inner arg2"); - result.CommandResult + result.CommandResultInternal .Parent .Tokens.Select(t => t.Value) .Should() .BeEquivalentTo("arg1"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -631,7 +631,7 @@ public void When_child_option_will_not_accept_arg_then_parent_can() var optionResult = result.GetResult(option); optionResult.Tokens.Should().BeEmpty(); - result.CommandResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); + result.CommandResultInternal.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); } [Fact] @@ -646,7 +646,7 @@ public void When_parent_option_will_not_accept_arg_then_child_can() var result = CliParser.Parse(command, "the-command -x the-argument"); result.GetResult(option).Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); - result.CommandResult.Tokens.Should().BeEmpty(); + result.CommandResultInternal.Tokens.Should().BeEmpty(); } [Fact] @@ -698,18 +698,18 @@ public void When_options_with_the_same_name_are_defined_on_parent_and_child_comm ParseResult result = CliParser.Parse(outer, "outer inner -x"); - result.CommandResult + result.CommandResultInternal .Parent .Should() - .BeOfType() + .BeOfType() .Which .Children .Should() - .AllBeAssignableTo(); - result.CommandResult + .AllBeAssignableTo(); + result.CommandResultInternal .Children .Should() - .ContainSingle(o => ((OptionResult)o).Option.Name == "-x"); + .ContainSingle(o => ((CliOptionResultInternal)o).Option.Name == "-x"); } [Fact] @@ -723,18 +723,18 @@ public void When_options_with_the_same_name_are_defined_on_parent_and_child_comm var result = CliParser.Parse(outer, "outer -x inner"); - result.CommandResult + result.CommandResultInternal .Children .Should() .BeEmpty(); - result.CommandResult + result.CommandResultInternal .Parent .Should() - .BeOfType() + .BeOfType() .Which .Children .Should() - .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); + .ContainSingle(o => o is CliOptionResultInternal && ((CliOptionResultInternal)o).Option.Name == "-x"); } /* @@ -754,12 +754,12 @@ public void Arguments_only_apply_to_the_nearest_command() ParseResult result = outer.Parse("outer inner arg1 arg2"); - result.CommandResult + result.CommandResultInternal .Parent .Tokens .Should() .BeEmpty(); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -814,7 +814,7 @@ public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_a "the-command" }); - CommandResult completeResult = result.CommandResult; + CliCommandResultInternal completeResult = result.CommandResultInternal; completeResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-command"); } @@ -832,7 +832,7 @@ public void Absolute_unix_style_paths_are_lexed_correctly() var result = CliParser.Parse(command, commandText); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -852,7 +852,7 @@ public void Absolute_Windows_style_paths_are_lexed_correctly() ParseResult result = CliParser.Parse(command, commandText); - result.CommandResult + result.CommandResultInternal .Tokens .Should() .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); @@ -996,7 +996,7 @@ public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_t ParseResult result = CliParser.Parse(outer, "outer inner -p:RandomThing=random"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -1042,32 +1042,32 @@ public void Option_and_Command_can_have_the_same_alias() }; CliParser.Parse(outerCommand, "outer inner") - .CommandResult + .CommandResultInternal .Command .Should() .BeSameAs(innerCommand); CliParser.Parse(outerCommand, "outer --inner") - .CommandResult + .CommandResultInternal .Command .Should() .BeSameAs(outerCommand); CliParser.Parse(outerCommand, "outer --inner inner") - .CommandResult + .CommandResultInternal .Command .Should() .BeSameAs(innerCommand); CliParser.Parse(outerCommand, "outer --inner inner") - .CommandResult + .CommandResultInternal .Parent .Should() - .BeOfType() + .BeOfType() .Which .Children .Should() - .Contain(o => ((OptionResult)o).Option == option); + .Contain(o => ((CliOptionResultInternal)o).Option == option); } [Fact] @@ -1082,14 +1082,14 @@ public void Options_can_have_the_same_alias_differentiated_only_by_prefix() option2 }; - CliParser.Parse(rootCommand, "-a").CommandResult + CliParser.Parse(rootCommand, "-a").CommandResultInternal .Children - .Select(s => ((OptionResult)s).Option) + .Select(s => ((CliOptionResultInternal)s).Option) .Should() .BeEquivalentTo(option1); - CliParser.Parse(rootCommand, "--a").CommandResult + CliParser.Parse(rootCommand, "--a").CommandResultInternal .Children - .Select(s => ((OptionResult)s).Option) + .Select(s => ((CliOptionResultInternal)s).Option) .Should() .BeEquivalentTo(option2); } @@ -1196,7 +1196,7 @@ public void Option_arguments_can_match_subcommands() var result = CliParser.Parse(rootCommand, "-a subcommand"); GetValue(result, optionA).Should().Be("subcommand"); - result.CommandResult.Command.Should().BeSameAs(rootCommand); + result.CommandResultInternal.Command.Should().BeSameAs(rootCommand); } [Fact] @@ -1214,7 +1214,7 @@ public void Arguments_can_match_subcommands() var result = CliParser.Parse(rootCommand, "subcommand one two three subcommand four"); - result.CommandResult.Command.Should().BeSameAs(subcommand); + result.CommandResultInternal.Command.Should().BeSameAs(subcommand); GetValue(result, argument) .Should() @@ -1406,12 +1406,12 @@ public void When_a_command_line_has_unmatched_tokens_the_parse_result_action_sho if (treatUnmatchedTokensAsErrors) { result.Errors.Should().NotBeEmpty(); - result.Action.Should().NotBeSameAs(result.CommandResult.Command.Action); + result.Action.Should().NotBeSameAs(result.CommandResultInternal.Command.Action); } else { result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + result.Action.Should().BeSameAs(result.CommandResultInternal.Command.Action); } } @@ -1435,7 +1435,7 @@ public void RootCommand_TreatUnmatchedTokensAsErrors_set_to_false_has_precedence result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + result.Action.Should().BeSameAs(result.CommandResultInternal.Command.Action); } */ @@ -1460,7 +1460,7 @@ public void Command_argument_arity_can_be_a_fixed_value_greater_than_1() }; CliParser.Parse(command, "1 2 3") - .CommandResult + .CommandResultInternal .Tokens .Should() .BeEquivalentTo( @@ -1482,7 +1482,7 @@ public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_tha }; CliParser.Parse(command, "1 2 3") - .CommandResult + .CommandResultInternal .Tokens .Should() .BeEquivalentTo( @@ -1490,7 +1490,7 @@ public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_tha new CliToken("2", CliTokenType.Argument, argument, dummyLocation), new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); CliParser.Parse(command, "1 2 3 4 5") - .CommandResult + .CommandResultInternal .Tokens .Should() .BeEquivalentTo( @@ -1710,7 +1710,7 @@ public void CommandResult_contains_argument_ValueResults() var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandResult = parseResult.CommandResult; + var commandResult = parseResult.CommandResultInternal; commandResult.ValueResults.Should().HaveCount(2); var result1 = commandResult.ValueResults[0]; result1.GetValue().Should().Be("Kirk"); @@ -1735,7 +1735,7 @@ public void CommandResult_contains_option_ValueResults() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); - var commandResult = parseResult.CommandResult; + var commandResult = parseResult.CommandResultInternal; commandResult.ValueResults.Should().HaveCount(2); var result1 = commandResult.ValueResults[0]; result1.GetValue().Should().Be("Kirk"); @@ -1763,7 +1763,7 @@ public void Location_in_ValueResult_correct_for_arguments() var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandResult = parseResult.CommandResult; + var commandResult = parseResult.CommandResultInternal; var result1 = commandResult.ValueResults[0]; var result2 = commandResult.ValueResults[1]; result1.Locations.Single().Should().Be(expectedLocation1); @@ -1790,7 +1790,7 @@ public void Location_in_ValueResult_correct_for_options() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); - var commandResult = parseResult.CommandResult; + var commandResult = parseResult.CommandResultInternal; var result1 = commandResult.ValueResults[0]; var result2 = commandResult.ValueResults[1]; result1.Locations.Single().Should().Be(expectedLocation1); @@ -1817,7 +1817,7 @@ public void Location_offsets_in_ValueResult_correct_for_arguments() var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandResult = parseResult.CommandResult; + var commandResult = parseResult.CommandResultInternal; var result1 = commandResult.ValueResults.Single(); result1.Locations.First().Should().Be(expectedLocation1); result1.Locations.Skip(1).Single().Should().Be(expectedLocation2); @@ -1841,7 +1841,7 @@ public void Location_offsets_in_ValueResult_correct_for_options() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt1 Spock"); - var commandResult = parseResult.CommandResult; + var commandResult = parseResult.CommandResultInternal; var result1 = commandResult.ValueResults.Single(); result1.Locations.First().Should().Be(expectedLocation1); result1.Locations.Skip(1).Single().Should().Be(expectedLocation2); @@ -1867,7 +1867,7 @@ public void Location_offset_correct_when_colon_or_equal_used() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1:Kirk --opt11=Spock"); - var commandResult = parseResult.CommandResult; + var commandResult = parseResult.CommandResultInternal; var result1 = commandResult.ValueResults[0]; var result2 = commandResult.ValueResults[1]; result1.Locations.Single().Should().Be(expectedLocation1); diff --git a/src/System.CommandLine.Tests/ParsingValidationTests.cs b/src/System.CommandLine.Tests/ParsingValidationTests.cs index bd9687b2d2..f9bf9e5281 100644 --- a/src/System.CommandLine.Tests/ParsingValidationTests.cs +++ b/src/System.CommandLine.Tests/ParsingValidationTests.cs @@ -46,9 +46,9 @@ public void When_an_option_has_en_error_then_the_error_has_a_reference_to_the_op var result = new CliRootCommand { option }.Parse("-x something_else"); result.Errors - .Where(e => e.SymbolResult != null) + .Where(e => e.SymbolResultInternal != null) .Should() - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == option.Name); + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == option.Name); } [Fact] // https://github.com/dotnet/command-line-api/issues/1475 @@ -67,7 +67,7 @@ public void When_FromAmong_is_used_then_the_OptionResult_ErrorMessage_is_set() .Should() .Be(LocalizationResources.UnrecognizedArgument("c", new []{ "a", "b"})); error - .SymbolResult + .SymbolResultInternal .Should() .BeOfType(); @@ -90,7 +90,7 @@ public void When_FromAmong_is_used_then_the_ArgumentResult_ErrorMessage_is_set() .Should() .Be(LocalizationResources.UnrecognizedArgument("c", new []{ "a", "b"})); error - .SymbolResult + .SymbolResultInternal .Should() .BeOfType(); } @@ -380,7 +380,7 @@ public void A_custom_validator_can_be_added_to_an_option() .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option == option) + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option == option) .Which .Message .Should() @@ -407,7 +407,7 @@ public void A_custom_validator_can_be_added_to_an_argument() .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument == argument) + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument == argument) .Which .Message .Should() @@ -476,7 +476,7 @@ public void Validators_on_global_options_are_executed_when_invoking_a_subcommand .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option == option) + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option == option) .Which .Message .Should() @@ -613,7 +613,7 @@ public void LegalFilePathsOnly_rejects_command_arguments_containing_invalid_path .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument == command.Arguments.First() && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument == command.Arguments.First() && e.Message == $"Character not allowed in a path: '{invalidCharacter}'."); } @@ -635,7 +635,7 @@ public void LegalFilePathsOnly_rejects_option_arguments_containing_invalid_path_ .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "-x" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "-x" && e.Message == $"Character not allowed in a path: '{invalidCharacter}'."); } @@ -698,7 +698,7 @@ public void LegalFileNamesOnly_rejects_command_arguments_containing_invalid_file .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument == command.Arguments.First() && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument == command.Arguments.First() && e.Message == $"Character not allowed in a file name: '{invalidCharacter}'."); } @@ -721,7 +721,7 @@ public void LegalFileNamesOnly_rejects_option_arguments_containing_invalid_file_ .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "-x" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "-x" && e.Message == $"Character not allowed in a file name: '{invalidCharacter}'."); } @@ -781,7 +781,7 @@ public void A_command_argument_can_be_invalid_based_on_file_existence() .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"File does not exist: '{path}'."); } @@ -800,7 +800,7 @@ public void An_option_argument_can_be_invalid_based_on_file_existence() .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"File does not exist: '{path}'."); } @@ -819,7 +819,7 @@ public void A_command_argument_can_be_invalid_based_on_directory_existence() .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"Directory does not exist: '{path}'."); } @@ -838,7 +838,7 @@ public void An_option_argument_can_be_invalid_based_on_directory_existence() .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"Directory does not exist: '{path}'."); } @@ -857,7 +857,7 @@ public void A_command_argument_can_be_invalid_based_on_file_or_directory_existen .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument == command.Arguments.First() && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument == command.Arguments.First() && e.Message == $"File or directory does not exist: '{path}'."); } @@ -876,7 +876,7 @@ public void An_option_argument_can_be_invalid_based_on_file_or_directory_existen .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"File or directory does not exist: '{path}'."); } @@ -895,7 +895,7 @@ public void A_command_argument_with_multiple_files_can_be_invalid_based_on_file_ .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"File does not exist: '{path}'."); } @@ -914,7 +914,7 @@ public void An_option_argument_with_multiple_files_can_be_invalid_based_on_file_ .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"File does not exist: '{path}'."); } @@ -933,7 +933,7 @@ public void A_command_argument_with_multiple_directories_can_be_invalid_based_on .Should() .HaveCount(1) .And - .ContainSingle(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .ContainSingle(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"Directory does not exist: '{path}'."); } @@ -952,7 +952,7 @@ public void An_option_argument_with_multiple_directories_can_be_invalid_based_on .Should() .HaveCount(1) .And - .ContainSingle(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .ContainSingle(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"Directory does not exist: '{path}'."); } @@ -973,7 +973,7 @@ public void A_command_argument_with_multiple_FileSystemInfos_can_be_invalid_base result.Errors .Should() - .ContainSingle(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .ContainSingle(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"File or directory does not exist: '{path}'."); } @@ -992,7 +992,7 @@ public void An_option_argument_with_multiple_FileSystemInfos_can_be_invalid_base result.Errors .Should() - .ContainSingle(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .ContainSingle(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"File or directory does not exist: '{path}'."); } @@ -1011,7 +1011,7 @@ public void A_command_argument_with_multiple_FileSystemInfos_can_be_invalid_base .Should() .HaveCount(1) .And - .ContainSingle(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .ContainSingle(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"File or directory does not exist: '{path}'."); } @@ -1030,7 +1030,7 @@ public void An_option_argument_with_multiple_FileSystemInfos_can_be_invalid_base .Should() .HaveCount(1) .And - .ContainSingle(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .ContainSingle(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"File or directory does not exist: '{path}'."); } @@ -1123,7 +1123,7 @@ public void A_command_with_subcommands_is_invalid_to_invoke_if_it_has_no_handler .Should() .ContainSingle( e => e.Message.Equals(LocalizationResources.RequiredCommandWasNotProvided()) && - ((CommandResult)e.SymbolResult).Command.Name.Equals("inner")); + ((CliCommandResultInternal)e.SymbolResultInternal).Command.Name.Equals("inner")); } [Fact] @@ -1139,7 +1139,7 @@ public void A_root_command_with_subcommands_is_invalid_to_invoke_if_it_has_no_ha .Should() .ContainSingle( e => e.Message.Equals(LocalizationResources.RequiredCommandWasNotProvided()) && - ((CommandResult)e.SymbolResult).Command == rootCommand); + ((CliCommandResultInternal)e.SymbolResultInternal).Command == rootCommand); } [Fact] @@ -1155,7 +1155,7 @@ public void A_command_with_subcommands_is_valid_to_invoke_if_it_has_a_handler() var result = outer.Parse("outer inner"); result.Errors.Should().BeEmpty(); - result.CommandResult.Command.Should().BeSameAs(inner); + result.CommandResultInternal.Command.Should().BeSameAs(inner); } [Fact] diff --git a/src/System.CommandLine.Tests/ResponseFileTests.cs b/src/System.CommandLine.Tests/ResponseFileTests.cs index 886116ea98..c3725ba443 100644 --- a/src/System.CommandLine.Tests/ResponseFileTests.cs +++ b/src/System.CommandLine.Tests/ResponseFileTests.cs @@ -87,7 +87,7 @@ public void When_response_file_is_specified_it_loads_command_arguments_from_resp } .Parse($"@{responseFile}"); - result.CommandResult + result.CliCommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -111,7 +111,7 @@ public void Response_file_can_provide_subcommand_arguments() } .Parse($"subcommand @{responseFile}"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -132,7 +132,7 @@ public void Response_file_can_provide_subcommand() } .Parse($"@{responseFile} one two three"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -156,7 +156,7 @@ public void When_response_file_is_specified_it_loads_subcommand_arguments_from_r } .Parse($"subcommand @{responseFile}"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() diff --git a/src/System.CommandLine/ArgumentArity.cs b/src/System.CommandLine/ArgumentArity.cs index efebad3713..6a96acdaf9 100644 --- a/src/System.CommandLine/ArgumentArity.cs +++ b/src/System.CommandLine/ArgumentArity.cs @@ -73,11 +73,11 @@ public bool Equals(ArgumentArity other) => public override int GetHashCode() => MaximumNumberOfValues ^ MinimumNumberOfValues ^ IsNonDefault.GetHashCode(); - internal static bool Validate(ArgumentResult argumentResult, [NotNullWhen(false)] out ArgumentConversionResult? error) + internal static bool Validate(CliArgumentResultInternal argumentResult, [NotNullWhen(false)] out ArgumentConversionResult? error) { error = null; - if (argumentResult.Parent is null or OptionResult { Implicit: true }) + if (argumentResult.Parent is null or CliOptionResultInternal { Implicit: true }) { return true; } @@ -95,7 +95,7 @@ internal static bool Validate(ArgumentResult argumentResult, [NotNullWhen(false) if (tokenCount > argumentResult.Argument.Arity.MaximumNumberOfValues) { - if (argumentResult.Parent is OptionResult optionResult) + if (argumentResult.Parent is CliOptionResultInternal optionResult) { if (!optionResult.Option.AllowMultipleArgumentsPerToken) { diff --git a/src/System.CommandLine/Binding/ArgumentConversionResult.cs b/src/System.CommandLine/Binding/ArgumentConversionResult.cs index 8c151c6d5d..7e03e86034 100644 --- a/src/System.CommandLine/Binding/ArgumentConversionResult.cs +++ b/src/System.CommandLine/Binding/ArgumentConversionResult.cs @@ -8,43 +8,43 @@ namespace System.CommandLine.Binding { internal sealed class ArgumentConversionResult { - internal readonly ArgumentResult ArgumentResult; + internal readonly CliArgumentResultInternal ArgumentResultInternal; internal readonly object? Value; internal readonly string? ErrorMessage; internal ArgumentConversionResultType Result; - private ArgumentConversionResult(ArgumentResult argumentResult, string error, ArgumentConversionResultType failure) + private ArgumentConversionResult(CliArgumentResultInternal argumentResult, string error, ArgumentConversionResultType failure) { - ArgumentResult = argumentResult; + ArgumentResultInternal = argumentResult; ErrorMessage = error; Result = failure; } - private ArgumentConversionResult(ArgumentResult argumentResult, object? value, ArgumentConversionResultType result) + private ArgumentConversionResult(CliArgumentResultInternal argumentResult, object? value, ArgumentConversionResultType result) { - ArgumentResult = argumentResult; + ArgumentResultInternal = argumentResult; Value = value; Result = result; } - internal static ArgumentConversionResult Failure(ArgumentResult argumentResult, string error, ArgumentConversionResultType reason) + internal static ArgumentConversionResult Failure(CliArgumentResultInternal argumentResult, string error, ArgumentConversionResultType reason) => new(argumentResult, error, reason); - internal static ArgumentConversionResult ArgumentConversionCannotParse(ArgumentResult argumentResult, Type expectedType, string value) + internal static ArgumentConversionResult ArgumentConversionCannotParse(CliArgumentResultInternal argumentResult, Type expectedType, string value) => new(argumentResult, FormatErrorMessage(argumentResult, expectedType, value), ArgumentConversionResultType.FailedType); - public static ArgumentConversionResult Success(ArgumentResult argumentResult, object? value) + public static ArgumentConversionResult Success(CliArgumentResultInternal argumentResult, object? value) => new(argumentResult, value, ArgumentConversionResultType.Successful); - internal static ArgumentConversionResult None(ArgumentResult argumentResult) + internal static ArgumentConversionResult None(CliArgumentResultInternal argumentResult) => new(argumentResult, value: null, ArgumentConversionResultType.NoArgument); private static string FormatErrorMessage( - ArgumentResult argumentResult, + CliArgumentResultInternal argumentResult, Type expectedType, string value) { - if (argumentResult.Parent is CommandResult commandResult) + if (argumentResult.Parent is CliCommandResultInternal commandResult) { string alias = commandResult.Command.Name; // TODO: completion @@ -62,7 +62,7 @@ private static string FormatErrorMessage( return LocalizationResources.ArgumentConversionCannotParseForCommand(value, alias, expectedType); } } - else if (argumentResult.Parent is OptionResult optionResult) + else if (argumentResult.Parent is CliOptionResultInternal optionResult) { string alias = optionResult.Option.Name; // TODO: completion diff --git a/src/System.CommandLine/Binding/ArgumentConverter.cs b/src/System.CommandLine/Binding/ArgumentConverter.cs index 9fa921348a..e70a0110d9 100644 --- a/src/System.CommandLine/Binding/ArgumentConverter.cs +++ b/src/System.CommandLine/Binding/ArgumentConverter.cs @@ -10,7 +10,7 @@ namespace System.CommandLine.Binding internal static partial class ArgumentConverter { internal static ArgumentConversionResult ConvertObject( - ArgumentResult argumentResult, + CliArgumentResultInternal argumentResult, Type type, object? value) { @@ -36,7 +36,7 @@ internal static ArgumentConversionResult ConvertObject( } private static ArgumentConversionResult ConvertToken( - ArgumentResult argumentResult, + CliArgumentResultInternal argumentResult, Type type, CliToken token) { @@ -81,7 +81,7 @@ private static ArgumentConversionResult ConvertToken( } private static ArgumentConversionResult ConvertTokens( - ArgumentResult argumentResult, + CliArgumentResultInternal argumentResult, Type type, IReadOnlyList tokens) { @@ -110,7 +110,7 @@ private static ArgumentConversionResult ConvertTokens( break; default: // failures - if (argumentResult.Parent is CommandResult) + if (argumentResult.Parent is CliCommandResultInternal) { argumentResult.OnlyTake(i); @@ -132,15 +132,15 @@ private static ArgumentConversionResult ConvertTokens( if (argument.ValueType.TryGetNullableType(out var nullableType) && StringConverters.TryGetValue(nullableType, out var convertNullable)) { - return (ArgumentResult result, out object? value) => ConvertSingleString(result, convertNullable, out value); + return (CliArgumentResultInternal result, out object? value) => ConvertSingleString(result, convertNullable, out value); } if (StringConverters.TryGetValue(argument.ValueType, out var convert1)) { - return (ArgumentResult result, out object? value) => ConvertSingleString(result, convert1, out value); + return (CliArgumentResultInternal result, out object? value) => ConvertSingleString(result, convert1, out value); } - static bool ConvertSingleString(ArgumentResult result, TryConvertString convert, out object? value) => + static bool ConvertSingleString(CliArgumentResultInternal result, TryConvertString convert, out object? value) => convert(result.Tokens[result.Tokens.Count - 1].Value, out value); } @@ -183,12 +183,12 @@ internal static ArgumentConversionResult ConvertIfNeeded( return conversionResult.Result switch { ArgumentConversionResultType.Successful when !toType.IsInstanceOfType(conversionResult.Value) => - ConvertObject(conversionResult.ArgumentResult, + ConvertObject(conversionResult.ArgumentResultInternal, toType, conversionResult.Value), - ArgumentConversionResultType.NoArgument when conversionResult.ArgumentResult.Argument.IsBoolean() => - Success(conversionResult.ArgumentResult, true), + ArgumentConversionResultType.NoArgument when conversionResult.ArgumentResultInternal.Argument.IsBoolean() => + Success(conversionResult.ArgumentResultInternal, true), _ => conversionResult }; @@ -204,7 +204,7 @@ internal static T GetValueOrDefault(this ArgumentConversionResult result) }; } - public static bool TryConvertArgument(ArgumentResult argumentResult, out object? value) + public static bool TryConvertArgument(CliArgumentResultInternal argumentResult, out object? value) { var argument = argumentResult.Argument; diff --git a/src/System.CommandLine/Binding/TryConvertArgument.cs b/src/System.CommandLine/Binding/TryConvertArgument.cs index 44d777e939..f8fb30d9f4 100644 --- a/src/System.CommandLine/Binding/TryConvertArgument.cs +++ b/src/System.CommandLine/Binding/TryConvertArgument.cs @@ -6,6 +6,6 @@ namespace System.CommandLine.Binding { internal delegate bool TryConvertArgument( - ArgumentResult argumentResult, + CliArgumentResultInternal argumentResult, out object? value); } \ No newline at end of file diff --git a/src/System.CommandLine/CliArgument.cs b/src/System.CommandLine/CliArgument.cs index 617f1d5b9b..721b9ca53b 100644 --- a/src/System.CommandLine/CliArgument.cs +++ b/src/System.CommandLine/CliArgument.cs @@ -115,10 +115,10 @@ public List>> CompletionSour /// Returns the default value for the argument, if defined. Null otherwise. public object? GetDefaultValue() { - return GetDefaultValue(new ArgumentResult(this, null!, null)); + return GetDefaultValue(new CliArgumentResultInternal(this, null!, null)); } - internal abstract object? GetDefaultValue(ArgumentResult argumentResult); + internal abstract object? GetDefaultValue(CliArgumentResultInternal argumentResult); /// /// Specifies if a default value is defined for the argument. diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index 77c31edb7b..d9ac656f1d 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -34,7 +34,7 @@ public CliArgument(string name) : base(name) /// the delegate is also invoked when an input was provided. /// */ - internal Func? DefaultValueFactory { get; set; } + internal Func? DefaultValueFactory { get; set; } // TODO: custom parsers /* @@ -81,7 +81,7 @@ public CliArgument(string name) : base(name) /// public override bool HasDefaultValue => DefaultValueFactory is not null; - internal override object? GetDefaultValue(ArgumentResult argumentResult) + internal override object? GetDefaultValue(CliArgumentResultInternal argumentResult) { if (DefaultValueFactory is null) { diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index b4e01e3909..8adccb5d9d 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -30,7 +30,7 @@ public class CliCommand : CliSymbol, IEnumerable private ChildSymbolList? _subcommands; // TODO: validators /* - private List>? _validators; + private List>? _validators; */ /// @@ -91,7 +91,7 @@ public IEnumerable Children /// Validators to the command. Validators can be used /// to create custom validation logic. /// - public List> Validators => _validators ??= new (); + public List> Validators => _validators ??= new (); internal bool HasValidators => _validators is not null && _validators.Count > 0; diff --git a/src/System.CommandLine/CliOption{T}.cs b/src/System.CommandLine/CliOption{T}.cs index 1a233183ad..f2ca874d1a 100644 --- a/src/System.CommandLine/CliOption{T}.cs +++ b/src/System.CommandLine/CliOption{T}.cs @@ -30,7 +30,7 @@ private protected CliOption(string name, string[] aliases, CliArgument argume } /// - internal Func? DefaultValueFactory + internal Func? DefaultValueFactory { get => _argument.DefaultValueFactory; set => _argument.DefaultValueFactory = value; diff --git a/src/System.CommandLine/LocalizationResources.cs b/src/System.CommandLine/LocalizationResources.cs index fc376d7cf4..cfde47ccbd 100644 --- a/src/System.CommandLine/LocalizationResources.cs +++ b/src/System.CommandLine/LocalizationResources.cs @@ -16,7 +16,7 @@ internal static class LocalizationResources /// /// Interpolates values into a localized string similar to Command '{0}' expects a single argument but {1} were provided. /// - internal static string ExpectsOneArgument(OptionResult optionResult) + internal static string ExpectsOneArgument(CliOptionResultInternal optionResult) => GetResourceString(Properties.Resources.OptionExpectsOneArgument, GetOptionName(optionResult), optionResult.Tokens.Count); /* /// @@ -52,15 +52,15 @@ internal static string InvalidCharactersInFileName(char invalidChar) => /// /// Interpolates values into a localized string similar to Required argument missing for command: {0}. /// - internal static string RequiredArgumentMissing(ArgumentResult argumentResult) => - argumentResult.Parent is CommandResult commandResult + internal static string RequiredArgumentMissing(CliArgumentResultInternal argumentResult) => + argumentResult.Parent is CliCommandResultInternal commandResult ? GetResourceString(Properties.Resources.CommandRequiredArgumentMissing, commandResult.IdentifierToken.Value) - : RequiredArgumentMissing((OptionResult)argumentResult.Parent!); + : RequiredArgumentMissing((CliOptionResultInternal)argumentResult.Parent!); /// /// Interpolates values into a localized string similar to Required argument missing for option: {0}. /// - internal static string RequiredArgumentMissing(OptionResult optionResult) => + internal static string RequiredArgumentMissing(CliOptionResultInternal optionResult) => GetResourceString(Properties.Resources.OptionRequiredArgumentMissing, GetOptionName(optionResult)); /// @@ -252,6 +252,6 @@ private static string GetResourceString(string resourceString, params object[] f return resourceString; } - private static string GetOptionName(OptionResult optionResult) => optionResult.IdentifierToken?.Value ?? optionResult.Option.Name; + private static string GetOptionName(CliOptionResultInternal optionResult) => optionResult.IdentifierToken?.Value ?? optionResult.Option.Name; } } diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 6a15a0ba61..061cfbe62f 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -14,10 +14,10 @@ namespace System.CommandLine /// public sealed class ParseResult { - private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); + private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); private SymbolLookupByName? symbolLookupByName = null; - private readonly CommandResult _rootCommandResult; + private readonly CliCommandResultInternal _rootCommandResult; // TODO: unmatched tokens, invocation, completion /* private readonly IReadOnlyList _unmatchedTokens; @@ -29,8 +29,8 @@ public sealed class ParseResult internal ParseResult( CliConfiguration configuration, // TODO: determine how rootCommandResult and commandResult differ - CommandResult rootCommandResult, - CommandResult commandResult, + CliCommandResultInternal rootCommandResult, + CliCommandResultInternal commandResult, SymbolResultTree symbolResultTree, /* List tokens, @@ -49,7 +49,7 @@ internal ParseResult( { Configuration = configuration; _rootCommandResult = rootCommandResult; - CommandResult = commandResult; + CommandResultInternal = commandResult; valueResultDictionary = symbolResultTree.BuildValueResultDictionary(); // TODO: invocation /* @@ -98,7 +98,7 @@ internal ParseResult( /// /// A result indicating the command specified in the command line input. /// - internal CommandResult CommandResult { get; } + internal CliCommandResultInternal CommandResultInternal { get; } /// /// The configuration used to produce the parse result. @@ -108,7 +108,7 @@ internal ParseResult( /// /// Gets the root command result. /// - internal CommandResult RootCommandResult => _rootCommandResult; + internal CliCommandResultInternal RootCommandResult => _rootCommandResult; /// /// Gets the parse errors found while parsing command line input. @@ -199,7 +199,7 @@ CommandLineText is null /// /// The option for which to find a result. /// A result for the specified option, or if it was not entered by the user. - public ValueResult? GetValueResult(CliOption option) + public CliValueResult? GetValueResult(CliOption option) => GetValueResultInternal(option); /// @@ -207,10 +207,10 @@ CommandLineText is null /// /// The argument for which to find a result. /// A result for the specified argument, or if it was not entered by the user. - public ValueResult? GetValueResult(CliArgument argument) + public CliValueResult? GetValueResult(CliArgument argument) => GetValueResultInternal(argument); - private ValueResult? GetValueResultInternal(CliSymbol symbol) + private CliValueResult? GetValueResultInternal(CliSymbol symbol) => valueResultDictionary.TryGetValue(symbol, out var result) ? result : null; @@ -220,7 +220,7 @@ CommandLineText is null /// /// The argument for which to find a result. /// A result for the specified argument, or if it was not provided and no default was configured. - internal ArgumentResult? GetResult(CliArgument argument) => + internal CliArgumentResultInternal? GetResult(CliArgument argument) => _rootCommandResult.GetResult(argument); /* Not used @@ -229,7 +229,7 @@ CommandLineText is null /// /// The command for which to find a result. /// A result for the specified command, or if it was not provided. - internal CommandResult? GetResult(CliCommand command) => + internal CliCommandResultInternal? GetResult(CliCommand command) => _rootCommandResult.GetResult(command); */ @@ -238,7 +238,7 @@ CommandLineText is null /// /// The option for which to find a result. /// A result for the specified option, or if it was not provided and no default was configured. - internal OptionResult? GetResult(CliOption option) => + internal CliOptionResultInternal? GetResult(CliOption option) => _rootCommandResult.GetResult(option); // TODO: Directives @@ -256,8 +256,8 @@ CommandLineText is null /// /// The symbol for which to find a result. /// A result for the specified symbol, or if it was not provided and no default was configured. - public SymbolResult? GetResult(CliSymbol symbol) - => _rootCommandResult.SymbolResultTree.TryGetValue(symbol, out SymbolResult? result) ? result : null; + public CliSymbolResultInternal? GetResult(CliSymbol symbol) + => _rootCommandResult.SymbolResultTree.TryGetValue(symbol, out CliSymbolResultInternal? result) ? result : null; */ // TODO: completion, invocation /* @@ -269,14 +269,14 @@ CommandLineText is null public IEnumerable GetCompletions( int? position = null) { - SymbolResult currentSymbolResult = SymbolToComplete(position); + CliSymbolResultInternal currentSymbolResult = SymbolToComplete(position); CliSymbol currentSymbol = currentSymbolResult switch { ArgumentResult argumentResult => argumentResult.Argument, OptionResult optionResult => optionResult.Option, DirectiveResult directiveResult => directiveResult.Directive, - _ => ((CommandResult)currentSymbolResult).Command + _ => ((CliCommandResultInternal)currentSymbolResult).Command }; var context = GetCompletionContext(); @@ -289,7 +289,7 @@ public IEnumerable GetCompletions( var completions = currentSymbol.GetCompletions(context); - string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult + string[] optionsWithArgumentLimitReached = currentSymbolResult is CliCommandResultInternal commandResult ? OptionsWithArgumentLimitReached(commandResult) : Array.Empty(); @@ -298,7 +298,7 @@ public IEnumerable GetCompletions( return completions; - static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => + static string[] OptionsWithArgumentLimitReached(CliCommandResultInternal commandResult) => commandResult .Children .OfType() @@ -355,13 +355,13 @@ public int Invoke() /// Gets the for parsed result. The handler represents the action /// that will be performed when the parse result is invoked. /// - public CliAction? Action => _action ?? CommandResult.Command.Action; + public CliAction? Action => _action ?? CliCommandResultInternal.Command.Action; internal IReadOnlyList? PreActions => _preActions; - private SymbolResult SymbolToComplete(int? position = null) + private CliSymbolResultInternal SymbolToComplete(int? position = null) { - var commandResult = CommandResult; + var commandResult = CliCommandResultInternal; var allSymbolResultsForCompletion = AllSymbolResultsForCompletion(); @@ -369,11 +369,11 @@ private SymbolResult SymbolToComplete(int? position = null) return currentSymbol; - IEnumerable AllSymbolResultsForCompletion() + IEnumerable AllSymbolResultsForCompletion() { foreach (var item in commandResult.AllSymbolResults()) { - if (item is CommandResult command) + if (item is CliCommandResultInternal command) { yield return command; } diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/CliArgumentResultInternal.cs similarity index 86% rename from src/System.CommandLine/Parsing/ArgumentResult.cs rename to src/System.CommandLine/Parsing/CliArgumentResultInternal.cs index 9e2248b870..826cdb7d3e 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/CliArgumentResultInternal.cs @@ -9,21 +9,21 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing an . /// - internal sealed class ArgumentResult : SymbolResult + internal sealed class CliArgumentResultInternal : CliSymbolResultInternal { private ArgumentConversionResult? _conversionResult; private bool _onlyTakeHasBeenCalled; - internal ArgumentResult( + internal CliArgumentResultInternal( CliArgument argument, SymbolResultTree symbolResultTree, - SymbolResult? parent) : base(symbolResultTree, parent) + CliSymbolResultInternal? parent) : base(symbolResultTree, parent) { Argument = argument ?? throw new ArgumentNullException(nameof(argument)); } - private ValueResult? _valueResult; - public ValueResult ValueResult + private CliValueResult? _valueResult; + public CliValueResult ValueResult { get { @@ -34,7 +34,7 @@ public ValueResult ValueResult var conversionValue = GetArgumentConversionResult().Value; var locations = Tokens.Select(token => token.Location).ToArray(); //TODO: Remove this wrapper later - _valueResult = new ValueResult(Argument, conversionValue, locations, ArgumentResult.GetValueResultOutcome(GetArgumentConversionResult()?.Result)); // null is temporary here + _valueResult = new CliValueResult(Argument, conversionValue, locations, CliArgumentResultInternal.GetValueResultOutcome(GetArgumentConversionResult()?.Result)); // null is temporary here } return _valueResult; } @@ -66,7 +66,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. + /// Thrown if this method is called by Option-owned CliArgumentResultInternal. public void OnlyTake(int numberOfTokens) { if (numberOfTokens < 0) @@ -79,9 +79,9 @@ public void OnlyTake(int numberOfTokens) throw new InvalidOperationException($"{nameof(OnlyTake)} can only be called once."); } - if (Parent is OptionResult) + if (Parent is CliOptionResultInternal) { - throw new NotSupportedException($"{nameof(OnlyTake)} is supported only for a {nameof(CliCommand)}-owned {nameof(ArgumentResult)}"); + throw new NotSupportedException($"{nameof(OnlyTake)} is supported only for a {nameof(CliCommand)}-owned {nameof(CliArgumentResultInternal)}"); } _onlyTakeHasBeenCalled = true; @@ -91,7 +91,7 @@ public void OnlyTake(int numberOfTokens) return; } - CommandResult parent = (CommandResult)Parent!; + CliCommandResultInternal parent = (CliCommandResultInternal)Parent!; var arguments = parent.Command.Arguments; int argumentIndex = arguments.IndexOf(Argument); int nextArgumentIndex = argumentIndex + 1; @@ -100,16 +100,16 @@ public void OnlyTake(int numberOfTokens) while (tokensToPass > 0 && nextArgumentIndex < arguments.Count) { CliArgument nextArgument = parent.Command.Arguments[nextArgumentIndex]; - ArgumentResult nextArgumentResult; + CliArgumentResultInternal nextArgumentResult; - if (SymbolResultTree.TryGetValue(nextArgument, out SymbolResult? symbolResult)) + if (SymbolResultTree.TryGetValue(nextArgument, out CliSymbolResultInternal? symbolResult)) { - nextArgumentResult = (ArgumentResult)symbolResult; + nextArgumentResult = (CliArgumentResultInternal)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); + nextArgumentResult = new CliArgumentResultInternal(nextArgument, SymbolResultTree, Parent); SymbolResultTree.Add(nextArgument, nextArgumentResult); } @@ -124,7 +124,7 @@ public void OnlyTake(int numberOfTokens) nextArgumentIndex++; } - CommandResult rootCommand = parent; + CliCommandResultInternal rootCommand = parent; // When_tokens_are_passed_on_by_custom_parser_on_last_argument_then_they_become_unmatched_tokens while (tokensToPass > 0) { @@ -136,7 +136,7 @@ public void OnlyTake(int numberOfTokens) } /// - public override string ToString() => $"{nameof(ArgumentResult)} {Argument.Name}: {string.Join(" ", Tokens.Select(t => $"<{t.Value}>"))}"; + public override string ToString() => $"{nameof(CliArgumentResultInternal)} {Argument.Name}: {string.Join(" ", Tokens.Select(t => $"<{t.Value}>"))}"; /// internal override void AddError(string errorMessage) @@ -233,8 +233,8 @@ ArgumentConversionResult ReportErrorIfNeeded(ArgumentConversionResult result) /// /// Since Option.Argument is an internal implementation detail, this ArgumentResult applies to the OptionResult in public API if the parent is an OptionResult. /// - private SymbolResult AppliesToPublicSymbolResult => - Parent is OptionResult optionResult ? optionResult : this; + private CliSymbolResultInternal AppliesToPublicSymbolResult => + Parent is CliOptionResultInternal optionResult ? optionResult : this; internal static ValueResultOutcome GetValueResultOutcome(ArgumentConversionResultType? resultType) => resultType switch diff --git a/src/System.CommandLine/Parsing/CommandValueResult.cs b/src/System.CommandLine/Parsing/CliCommandResult.cs similarity index 72% rename from src/System.CommandLine/Parsing/CommandValueResult.cs rename to src/System.CommandLine/Parsing/CliCommandResult.cs index 60f403b5c1..45657e5a38 100644 --- a/src/System.CommandLine/Parsing/CommandValueResult.cs +++ b/src/System.CommandLine/Parsing/CliCommandResult.cs @@ -9,16 +9,16 @@ namespace System.CommandLine.Parsing; /// Provides the publicly facing command result /// /// -/// The name is temporary as we expect to later name this CommandResult and the previous one to CommandResultInternal +/// The name is temporary as we expect to later name this CliCommandResultInternal and the previous one to CommandResultInternal /// -public class CommandValueResult +public class CliCommandResult : CliSymbolResult { /// /// Creates a CommandValueResult instance /// /// The CliCommand that the result is for. /// The parent command in the case of a CLI hierarchy, or null if there is no parent. - internal CommandValueResult(CliCommand command, CommandValueResult? parent = null) + internal CliCommandResult(CliCommand command, CliCommandResult? parent = null) { Command = command; Parent = parent; @@ -27,7 +27,7 @@ internal CommandValueResult(CliCommand command, CommandValueResult? parent = nul /// /// The ValueResult instances for user entered data. This is a sparse list. /// - public IEnumerable ValueResults { get; } = new List(); + public IEnumerable ValueResults { get; } = new List(); /// /// The CliCommand that the result is for. @@ -37,6 +37,6 @@ internal CommandValueResult(CliCommand command, CommandValueResult? parent = nul /// /// The command's parent if one exists, otherwise, null /// - public CommandValueResult? Parent { get; } + public CliCommandResult? Parent { get; } } diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CliCommandResultInternal.cs similarity index 79% rename from src/System.CommandLine/Parsing/CommandResult.cs rename to src/System.CommandLine/Parsing/CliCommandResultInternal.cs index f486794eba..2cfd7d61fe 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CliCommandResultInternal.cs @@ -9,13 +9,14 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing a . /// - internal sealed class CommandResult : SymbolResult + internal sealed class CliCommandResultInternal + : CliSymbolResultInternal { - internal CommandResult( + internal CliCommandResultInternal( CliCommand command, CliToken token, SymbolResultTree symbolResultTree, - CommandResult? parent = null) : + CliCommandResultInternal? parent = null) : base(symbolResultTree, parent) { Command = command ?? throw new ArgumentNullException(nameof(command)); @@ -36,22 +37,22 @@ internal CommandResult( /// /// Child symbol results in the parse tree. /// - public IEnumerable Children => SymbolResultTree.GetChildren(this); + public IEnumerable Children => SymbolResultTree.GetChildren(this); - public IReadOnlyList ValueResults => Children.Select(GetValueResult).OfType().ToList(); + public IReadOnlyList ValueResults => Children.Select(GetValueResult).OfType().ToList(); - private ValueResult? GetValueResult(SymbolResult symbolResult) + private CliValueResult? GetValueResult(CliSymbolResultInternal symbolResult) => symbolResult switch { - ArgumentResult argumentResult => argumentResult.ValueResult, - OptionResult optionResult => optionResult.ValueResult, + CliArgumentResultInternal argumentResult => argumentResult.ValueResult, + CliOptionResultInternal optionResult => optionResult.ValueResult, _ => null! }; /// - public override string ToString() => $"{nameof(CommandResult)}: {IdentifierToken.Value} {string.Join(" ", Tokens.Select(t => t.Value))}"; + public override string ToString() => $"{nameof(CliCommandResultInternal)}: {IdentifierToken.Value} {string.Join(" ", Tokens.Select(t => t.Value))}"; - internal override bool UseDefaultValueFor(ArgumentResult argumentResult) + internal override bool UseDefaultValueFor(CliArgumentResultInternal argumentResult) => argumentResult.Argument.HasDefaultValue && argumentResult.Tokens.Count == 0; /// Only the inner most command goes through complete validation. @@ -111,10 +112,10 @@ private void ValidateOptions(bool completeValidation) continue; } - OptionResult optionResult; - ArgumentResult argumentResult; + CliOptionResultInternal optionResult; + CliArgumentResultInternal argumentResult; - if (!SymbolResultTree.TryGetValue(option, out SymbolResult? symbolResult)) + if (!SymbolResultTree.TryGetValue(option, out CliSymbolResultInternal? symbolResult)) { if (option.Required || option.Argument.HasDefaultValue) { @@ -137,8 +138,8 @@ private void ValidateOptions(bool completeValidation) } else { - optionResult = (OptionResult)symbolResult; - argumentResult = (ArgumentResult)SymbolResultTree[option.Argument]; + optionResult = (CliOptionResultInternal)symbolResult; + argumentResult = (CliArgumentResultInternal)SymbolResultTree[option.Argument]; } // When_there_is_an_arity_error_then_further_errors_are_not_reported @@ -186,14 +187,14 @@ private void ValidateArguments(bool completeValidation) continue; } - ArgumentResult? argumentResult; - if (SymbolResultTree.TryGetValue(argument, out SymbolResult? symbolResult)) + CliArgumentResultInternal? argumentResult; + if (SymbolResultTree.TryGetValue(argument, out CliSymbolResultInternal? symbolResult)) { - argumentResult = (ArgumentResult)symbolResult; + argumentResult = (CliArgumentResultInternal)symbolResult; } else if (argument.HasDefaultValue || argument.Arity.MinimumNumberOfValues > 0) { - argumentResult = new ArgumentResult(argument, SymbolResultTree, this); + argumentResult = new CliArgumentResultInternal(argument, SymbolResultTree, this); SymbolResultTree[argument] = argumentResult; if (!argument.HasDefaultValue && argument.Arity.MinimumNumberOfValues > 0) diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/CliOptionResultInternal.cs similarity index 83% rename from src/System.CommandLine/Parsing/OptionResult.cs rename to src/System.CommandLine/Parsing/CliOptionResultInternal.cs index 9bfa62babb..7050bb7894 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/CliOptionResultInternal.cs @@ -10,23 +10,23 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing an . /// - internal sealed class OptionResult : SymbolResult + internal sealed class CliOptionResultInternal : CliSymbolResultInternal { private ArgumentConversionResult? _argumentConversionResult; - internal OptionResult( + internal CliOptionResultInternal( CliOption option, SymbolResultTree symbolResultTree, CliToken? token = null, - CommandResult? parent = null) : + CliCommandResultInternal? parent = null) : base(symbolResultTree, parent) { Option = option ?? throw new ArgumentNullException(nameof(option)); IdentifierToken = token; } - private ValueResult? _valueResult; - public ValueResult ValueResult + private CliValueResult? _valueResult; + public CliValueResult ValueResult { get { @@ -44,7 +44,7 @@ public ValueResult ValueResult }; var locations = Tokens.Select(token => token.Location).ToArray(); //TODO: Remove this wrapper later - _valueResult = new ValueResult(Option, conversionValue, locations, ArgumentResult.GetValueResultOutcome(ArgumentConversionResult?.Result), conversionResult.ErrorMessage); + _valueResult = new CliValueResult(Option, conversionValue, locations, CliArgumentResultInternal.GetValueResultOutcome(ArgumentConversionResult?.Result), conversionResult.ErrorMessage); } return _valueResult; } @@ -76,7 +76,7 @@ public ValueResult ValueResult public int IdentifierTokenCount { get; internal set; } */ /// - public override string ToString() => $"{nameof(OptionResult)}: {IdentifierToken?.Value ?? Option.Name} {string.Join(" ", Tokens.Select(t => t.Value))}"; + public override string ToString() => $"{nameof(CliOptionResultInternal)}: {IdentifierToken?.Value ?? Option.Name} {string.Join(" ", Tokens.Select(t => t.Value))}"; /// /// Gets the parsed value or the default value for . @@ -93,6 +93,6 @@ internal bool IsArgumentLimitReached internal ArgumentConversionResult ArgumentConversionResult => _argumentConversionResult ??= GetResult(Option.Argument)!.GetArgumentConversionResult(); - internal override bool UseDefaultValueFor(ArgumentResult argument) => Implicit; + internal override bool UseDefaultValueFor(CliArgumentResultInternal argument) => Implicit; } } diff --git a/src/System.CommandLine/Parsing/CliSymbolResult.cs b/src/System.CommandLine/Parsing/CliSymbolResult.cs new file mode 100644 index 0000000000..74284738c1 --- /dev/null +++ b/src/System.CommandLine/Parsing/CliSymbolResult.cs @@ -0,0 +1,9 @@ +// 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 +{ + public class CliSymbolResult + { + } +} \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/CliSymbolResultInternal.cs similarity index 88% rename from src/System.CommandLine/Parsing/SymbolResult.cs rename to src/System.CommandLine/Parsing/CliSymbolResultInternal.cs index f32bec67c4..4b4ddd3653 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/CliSymbolResultInternal.cs @@ -8,13 +8,13 @@ namespace System.CommandLine.Parsing /// /// A result produced during parsing for a specific symbol. /// - internal abstract class SymbolResult + internal abstract class CliSymbolResultInternal { // TODO: make this a property and protected if possible internal readonly SymbolResultTree SymbolResultTree; private protected List? _tokens; - private protected SymbolResult(SymbolResultTree symbolResultTree, SymbolResult? parent) + private protected CliSymbolResultInternal(SymbolResultTree symbolResultTree, CliSymbolResultInternal? parent) { SymbolResultTree = symbolResultTree; Parent = parent; @@ -38,7 +38,7 @@ public IEnumerable Errors for (var i = 0; i < parseErrors.Count; i++) { var parseError = parseErrors[i]; - if (parseError.SymbolResult == this) + if (parseError.CliSymbolResultInternal == this) { yield return parseError; } @@ -49,7 +49,7 @@ public IEnumerable Errors /// /// The parent symbol result in the parse tree. /// - public SymbolResult? Parent { get; } + public CliSymbolResultInternal? Parent { get; } // TODO: make internal because exposes tokens /// @@ -70,7 +70,7 @@ public IEnumerable Errors /// /// The argument for which to find a result. /// An argument result if the argument was matched by the parser or has a default value; otherwise, null. - internal ArgumentResult? GetResult(CliArgument argument) => SymbolResultTree.GetResult(argument); + internal CliArgumentResultInternal? GetResult(CliArgument argument) => SymbolResultTree.GetResult(argument); /* Not used /// @@ -78,7 +78,7 @@ public IEnumerable Errors /// /// The command for which to find a result. /// An command result if the command was matched by the parser; otherwise, null. - internal CommandResult? GetResult(CliCommand command) => SymbolResultTree.GetResult(command); + internal CliCommandResultInternal? GetResult(CliCommand command) => SymbolResultTree.GetResult(command); */ /// @@ -86,7 +86,7 @@ public IEnumerable Errors /// /// The option for which to find a result. /// An option result if the option was matched by the parser or has a default value; otherwise, null. - internal OptionResult? GetResult(CliOption option) => SymbolResultTree.GetResult(option); + internal CliOptionResultInternal? GetResult(CliOption option) => SymbolResultTree.GetResult(option); // TODO: directives /* @@ -103,7 +103,7 @@ public IEnumerable Errors /// /// The name of the symbol for which to find a result. /// An argument result if the argument was matched by the parser or has a default value; otherwise, null. - public SymbolResult? GetResult(string name) => + public CliSymbolResultInternal? GetResult(string name) => SymbolResultTree.GetResult(name); /// @@ -156,6 +156,6 @@ public IEnumerable Errors } */ - internal virtual bool UseDefaultValueFor(ArgumentResult argumentResult) => false; + internal virtual bool UseDefaultValueFor(CliArgumentResultInternal argumentResult) => false; } } diff --git a/src/System.CommandLine/Parsing/ValueResult.cs b/src/System.CommandLine/Parsing/CliValueResult.cs similarity index 95% rename from src/System.CommandLine/Parsing/ValueResult.cs rename to src/System.CommandLine/Parsing/CliValueResult.cs index 04cc3114b9..96ab4c6a45 100644 --- a/src/System.CommandLine/Parsing/ValueResult.cs +++ b/src/System.CommandLine/Parsing/CliValueResult.cs @@ -8,9 +8,9 @@ namespace System.CommandLine.Parsing; /// /// The publicly facing class for argument and option data. /// -public class ValueResult +public class CliValueResult : CliSymbolResult { - private ValueResult( + private CliValueResult( CliSymbol valueSymbol, object? value, IEnumerable locations, @@ -34,7 +34,7 @@ private ValueResult( /// The locations list. /// True if parsing and converting the value was successful. /// The CliError if parsing or converting failed, otherwise null. - internal ValueResult( + internal CliValueResult( CliArgument argument, object? value, IEnumerable locations, @@ -52,7 +52,7 @@ internal ValueResult( /// The locations list. /// True if parsing and converting the value was successful. /// The CliError if parsing or converting failed, otherwise null. - internal ValueResult( + internal CliValueResult( CliOption option, object? value, IEnumerable locations, @@ -124,7 +124,7 @@ public IEnumerable TextForCommandReconstruction() /// The text the user entered that resulted in this ValueResult. /// public override string ToString() - => $"{nameof(ArgumentResult)} {ValueSymbol.Name}: {string.Join(" ", TextForDisplay())}"; + => $"{nameof(CliArgumentResultInternal)} {ValueSymbol.Name}: {string.Join(" ", TextForDisplay())}"; // TODO: This might not be the right place for this, (Some completion stuff was stripped out. This was a private method in ArgumentConversionResult) diff --git a/src/System.CommandLine/Parsing/DirectiveResult.cs b/src/System.CommandLine/Parsing/DirectiveResult.cs index a1d08544a7..3479071239 100644 --- a/src/System.CommandLine/Parsing/DirectiveResult.cs +++ b/src/System.CommandLine/Parsing/DirectiveResult.cs @@ -5,7 +5,7 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing an . /// - public sealed class DirectiveResult : SymbolResult + public sealed class DirectiveResult : CliSymbolResultInternal { private List? _values; diff --git a/src/System.CommandLine/Parsing/ParseDiagramAction.cs b/src/System.CommandLine/Parsing/ParseDiagramAction.cs index 474fcfd3af..d8a7b30296 100644 --- a/src/System.CommandLine/Parsing/ParseDiagramAction.cs +++ b/src/System.CommandLine/Parsing/ParseDiagramAction.cs @@ -53,10 +53,10 @@ internal static StringBuilder Diagram(ParseResult parseResult) private static void Diagram( StringBuilder builder, - SymbolResult symbolResult, + CliSymbolResultInternal symbolResult, ParseResult parseResult) { - if (parseResult.Errors.Any(e => e.SymbolResult == symbolResult)) + if (parseResult.Errors.Any(e => e.SymbolResultInternal == symbolResult)) { builder.Append('!'); } @@ -143,10 +143,10 @@ private static void Diagram( } else { - builder.Append(((CommandResult)symbolResult).IdentifierToken.Value); + builder.Append(((CliCommandResultInternal)symbolResult).IdentifierToken.Value); } - foreach (SymbolResult child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) + foreach (CliSymbolResultInternal child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) { if (child is ArgumentResult arg && (arg.Argument.ValueType == typeof(bool) || diff --git a/src/System.CommandLine/Parsing/ParseError.cs b/src/System.CommandLine/Parsing/ParseError.cs index 812a2b209c..9be6314ef3 100644 --- a/src/System.CommandLine/Parsing/ParseError.cs +++ b/src/System.CommandLine/Parsing/ParseError.cs @@ -9,10 +9,10 @@ namespace System.CommandLine.Parsing public sealed class ParseError { // TODO: add position - // TODO: reevaluate whether we should be exposing a SymbolResult here + // TODO: reevaluate whether we should be exposing a CliSymbolResultInternal here internal ParseError( string message, - SymbolResult? symbolResult = null) + CliSymbolResultInternal? symbolResult = null) { if (string.IsNullOrWhiteSpace(message)) { @@ -21,7 +21,7 @@ internal ParseError( Message = message; /* - SymbolResult = symbolResult; + CliSymbolResultInternal = symbolResult; */ } @@ -45,7 +45,7 @@ public ParseError( /// /// The symbol result detailing the symbol that failed to parse and the tokens involved. /// - public SymbolResult? SymbolResult { get; } + public CliSymbolResultInternal? CliSymbolResultInternal { get; } */ /// diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 8e972d2728..869fd436ae 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -11,10 +11,10 @@ internal sealed class ParseOperation private readonly CliConfiguration _configuration; private readonly string? _rawInput; private readonly SymbolResultTree _symbolResultTree; - private readonly CommandResult _rootCommandResult; + private readonly CliCommandResultInternal _rootCommandResult; private int _index; - private CommandResult _innermostCommandResult; + private CliCommandResultInternal _innermostCommandResult; /* private bool _isHelpRequested; private bool _isTerminatingDirectiveSpecified; @@ -36,7 +36,7 @@ public ParseOperation( _rawInput = rawInput; _symbolResultTree = new(rootCommand, tokenizationErrors); - _innermostCommandResult = _rootCommandResult = new CommandResult( + _innermostCommandResult = _rootCommandResult = new CliCommandResultInternal( rootCommand, CurrentToken, _symbolResultTree); @@ -105,7 +105,7 @@ private void ParseSubcommand() { CliCommand command = (CliCommand)CurrentToken.Symbol!; - _innermostCommandResult = new CommandResult( + _innermostCommandResult = new CliCommandResultInternal( command, CurrentToken, _symbolResultTree, @@ -162,10 +162,10 @@ private void ParseCommandArguments(ref int currentArgumentCount, ref int current } if (!(_symbolResultTree.TryGetValue(argument, out var symbolResult) - && symbolResult is ArgumentResult argumentResult)) + && symbolResult is CliArgumentResultInternal argumentResult)) { argumentResult = - new ArgumentResult( + new CliArgumentResultInternal( argument, _symbolResultTree, _innermostCommandResult); @@ -200,9 +200,9 @@ private void ParseCommandArguments(ref int currentArgumentCount, ref int current private void ParseOption() { CliOption option = (CliOption)CurrentToken.Symbol!; - OptionResult optionResult; + CliOptionResultInternal optionResult; - if (!_symbolResultTree.TryGetValue(option, out SymbolResult? symbolResult)) + if (!_symbolResultTree.TryGetValue(option, out CliSymbolResultInternal? symbolResult)) { // TODO: invocation, directives, help /* @@ -227,7 +227,7 @@ private void ParseOption() } } */ - optionResult = new OptionResult( + optionResult = new CliOptionResultInternal( option, _symbolResultTree, CurrentToken, @@ -237,7 +237,7 @@ private void ParseOption() } else { - optionResult = (OptionResult)symbolResult; + optionResult = (CliOptionResultInternal)symbolResult; } // TODO: IdentifierTokenCount @@ -248,7 +248,7 @@ private void ParseOption() ParseOptionArguments(optionResult); } - private void ParseOptionArguments(OptionResult optionResult) + private void ParseOptionArguments(CliOptionResultInternal optionResult) { var argument = optionResult.Option.Argument; @@ -275,10 +275,10 @@ private void ParseOptionArguments(OptionResult optionResult) break; } - if (!(_symbolResultTree.TryGetValue(argument, out SymbolResult? symbolResult) - && symbolResult is ArgumentResult argumentResult)) + if (!(_symbolResultTree.TryGetValue(argument, out CliSymbolResultInternal? symbolResult) + && symbolResult is CliArgumentResultInternal argumentResult)) { - argumentResult = new ArgumentResult( + argumentResult = new CliArgumentResultInternal( argument, _symbolResultTree, optionResult); @@ -305,7 +305,7 @@ private void ParseOptionArguments(OptionResult optionResult) { if (!_symbolResultTree.ContainsKey(argument)) { - var argumentResult = new ArgumentResult(argument, _symbolResultTree, optionResult); + var argumentResult = new CliArgumentResultInternal(argument, _symbolResultTree, optionResult); _symbolResultTree.Add(argument, argumentResult); } } @@ -395,12 +395,12 @@ private void Validate() // for other commands only a subset of options is checked. _innermostCommandResult.Validate(completeValidation: true); - CommandResult? currentResult = _innermostCommandResult.Parent as CommandResult; + CliCommandResultInternal? currentResult = _innermostCommandResult.Parent as CliCommandResultInternal; while (currentResult is not null) { currentResult.Validate(completeValidation: false); - currentResult = currentResult.Parent as CommandResult; + currentResult = currentResult.Parent as CliCommandResultInternal; } } } diff --git a/src/System.CommandLine/Parsing/SymbolLookupByName.cs b/src/System.CommandLine/Parsing/SymbolLookupByName.cs index eab4c8ac99..414157c10d 100644 --- a/src/System.CommandLine/Parsing/SymbolLookupByName.cs +++ b/src/System.CommandLine/Parsing/SymbolLookupByName.cs @@ -43,7 +43,7 @@ private List BuildCache(ParseResult parseResult) return cache; } cache = []; - var commandResult = parseResult.CommandResult; + var commandResult = parseResult.CommandResultInternal; while (commandResult is not null) { var command = commandResult.Command; @@ -57,7 +57,7 @@ private List BuildCache(ParseResult parseResult) AddSymbolsToCache(commandCache, command.Options, command); AddSymbolsToCache(commandCache, command.Arguments, command); AddSymbolsToCache(commandCache, command.Subcommands, command); - commandResult = (CommandResult?)commandResult.Parent; + commandResult = (CliCommandResultInternal?)commandResult.Parent; } return cache; @@ -95,7 +95,7 @@ private bool TryGetSymbolAndParentInternal(string name, bool skipAncestors, bool valuesOnly) { - startCommand ??= cache.First().Command; // The construction of the dictionary makes this the parseResult.CommandResult - current command + startCommand ??= cache.First().Command; // The construction of the dictionary makes this the parseResult.CliCommandResultInternal - current command var commandCaches = GetCommandCachesToUse(startCommand); if (commandCaches is null || !commandCaches.Any()) { diff --git a/src/System.CommandLine/Parsing/SymbolResultExtensions.cs b/src/System.CommandLine/Parsing/SymbolResultExtensions.cs index c81459e8c6..ce69867f8c 100644 --- a/src/System.CommandLine/Parsing/SymbolResultExtensions.cs +++ b/src/System.CommandLine/Parsing/SymbolResultExtensions.cs @@ -5,9 +5,9 @@ namespace System.CommandLine.Parsing { - internal static class SymbolResultExtensions + internal static class SymbolResultInternalExtensions { - internal static IEnumerable AllSymbolResults(this CommandResult commandResult) + internal static IEnumerable AllSymbolResults(this CliCommandResultInternal commandResult) { yield return commandResult; diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 74f2501a4d..651890925a 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -6,7 +6,7 @@ namespace System.CommandLine.Parsing { - internal sealed class SymbolResultTree : Dictionary + internal sealed class SymbolResultTree : Dictionary { private readonly CliCommand _rootCommand; internal List? Errors; @@ -37,22 +37,22 @@ internal SymbolResultTree( internal int ErrorCount => Errors?.Count ?? 0; - internal ArgumentResult? GetResult(CliArgument argument) - => TryGetValue(argument, out SymbolResult? result) ? (ArgumentResult)result : default; + internal CliArgumentResultInternal? GetResult(CliArgument argument) + => TryGetValue(argument, out CliSymbolResultInternal? result) ? (CliArgumentResultInternal)result : default; - internal CommandResult? GetResult(CliCommand command) - => TryGetValue(command, out var result) ? (CommandResult)result : default; + internal CliCommandResultInternal? GetResult(CliCommand command) + => TryGetValue(command, out var result) ? (CliCommandResultInternal)result : default; - internal OptionResult? GetResult(CliOption option) - => TryGetValue(option, out SymbolResult? result) ? (OptionResult)result : default; + internal CliOptionResultInternal? GetResult(CliOption option) + => TryGetValue(option, out CliSymbolResultInternal? result) ? (CliOptionResultInternal)result : default; // TODO: Determine how this is used. It appears to be O^n in the size of the tree and so if it is called multiple times, we should reconsider to avoid O^(N*M) - internal IEnumerable GetChildren(SymbolResult parent) + internal IEnumerable GetChildren(CliSymbolResultInternal parent) { // Argument can't have children - if (parent is not ArgumentResult) + if (parent is not CliArgumentResultInternal) { - foreach (KeyValuePair pair in this) + foreach (KeyValuePair pair in this) { if (ReferenceEquals(parent, pair.Value.Parent)) { @@ -62,18 +62,18 @@ internal IEnumerable GetChildren(SymbolResult parent) } } - internal IReadOnlyDictionary BuildValueResultDictionary() + internal IReadOnlyDictionary BuildValueResultDictionary() { - var dict = new Dictionary(); - foreach (KeyValuePair pair in this) + var dict = new Dictionary(); + foreach (KeyValuePair pair in this) { var result = pair.Value; - if (result is OptionResult optionResult) + if (result is CliOptionResultInternal optionResult) { dict.Add(pair.Key, optionResult.ValueResult); continue; } - if (result is ArgumentResult argumentResult) + if (result is CliArgumentResultInternal argumentResult) { dict.Add(pair.Key, argumentResult.ValueResult); continue; @@ -85,7 +85,7 @@ internal IReadOnlyDictionary BuildValueResultDictionary( internal void AddError(ParseError parseError) => (Errors ??= new()).Add(parseError); internal void InsertFirstError(ParseError parseError) => (Errors ??= new()).Insert(0, parseError); - internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, CommandResult rootCommandResult) + internal void AddUnmatchedToken(CliToken token, CliCommandResultInternal commandResult, CliCommandResultInternal rootCommandResult) { /* // TODO: unmatched tokens diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 9e45744e6b..56edce283d 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -27,7 +27,8 @@ - + + @@ -49,19 +50,19 @@ - + - + - + - + - + From fa502df3912ec62535a4b76f7bf2af38d5806c4b Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Tue, 13 Aug 2024 17:05:38 -0400 Subject: [PATCH 112/150] Updated CliSymbolResult and moved Locations there --- .../Parsing/CliCommandResult.cs | 7 ++++- .../Parsing/CliSymbolResult.cs | 26 +++++++++++++++---- .../Parsing/CliValueResult.cs | 10 +------ 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/System.CommandLine/Parsing/CliCommandResult.cs b/src/System.CommandLine/Parsing/CliCommandResult.cs index 45657e5a38..4a0531e5a2 100644 --- a/src/System.CommandLine/Parsing/CliCommandResult.cs +++ b/src/System.CommandLine/Parsing/CliCommandResult.cs @@ -17,8 +17,13 @@ public class CliCommandResult : CliSymbolResult /// Creates a CommandValueResult instance /// /// The CliCommand that the result is for. + /// /// The parent command in the case of a CLI hierarchy, or null if there is no parent. - internal CliCommandResult(CliCommand command, CliCommandResult? parent = null) + internal CliCommandResult( + CliCommand command, + IEnumerable locations, + CliCommandResult? parent = null) + : base(locations) { Command = command; Parent = parent; diff --git a/src/System.CommandLine/Parsing/CliSymbolResult.cs b/src/System.CommandLine/Parsing/CliSymbolResult.cs index 74284738c1..35353a7987 100644 --- a/src/System.CommandLine/Parsing/CliSymbolResult.cs +++ b/src/System.CommandLine/Parsing/CliSymbolResult.cs @@ -1,9 +1,25 @@ // 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 +using System.Collections.Generic; + +namespace System.CommandLine.Parsing; + +/// +/// Base class for CliValueResult and CliCommandResult. +/// +/// +/// Common values such as `TextForDisplay` are expected +/// +public abstract class CliSymbolResult(IEnumerable locations) { - public class CliSymbolResult - { - } -} \ No newline at end of file + /// + /// Gets the locations at which the tokens that made up the value appeared. + /// + /// + /// This needs to be a collection for CliValueType because collection types have + /// multiple tokens and they will not be simple offsets when response files are used. + /// + public IEnumerable Locations { get; } = locations; + +} diff --git a/src/System.CommandLine/Parsing/CliValueResult.cs b/src/System.CommandLine/Parsing/CliValueResult.cs index 96ab4c6a45..a894154444 100644 --- a/src/System.CommandLine/Parsing/CliValueResult.cs +++ b/src/System.CommandLine/Parsing/CliValueResult.cs @@ -17,10 +17,10 @@ private CliValueResult( ValueResultOutcome outcome, // TODO: Error should be an Enumerable and perhaps should not be here at all, only on ParseResult string? error = null) + : base(locations) { ValueSymbol = valueSymbol; Value = value; - Locations = locations; Outcome = outcome; // TODO: Probably a collection of errors here Error = error; @@ -79,14 +79,6 @@ internal CliValueResult( ? default : (T?)Value; - /// - /// Gets the locations at which the tokens that made up the value appeared. - /// - /// - /// This needs to be a collection because collection types have multiple tokens and they will not be simple offsets when response files are used. - /// - public IEnumerable Locations { get; } - /// /// True when parsing and converting the value was successful /// From 841c24e2d11e67969ebe9873d432642adf8e5c50 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Tue, 13 Aug 2024 16:20:50 -0400 Subject: [PATCH 113/150] Renamed result files and created CliSymbolResult base CommandResult -> CliCommandResultInternal OptionResult -> CliOptionResultInternal ArgumentResult -> CliArgumentResultInternal SymbolResult -> CliSymbolResultInternal ValueResult -> CliValueResult CommandValueResult -> CliCommandResult Created CliSymbolResult as abase class for ValueResult and CommandValueResult --- .../Help/HelpOptionAction.cs | 2 +- .../Invocation/ParseErrorAction.cs | 6 +- .../VersionOption.cs | 2 +- .../BindingContextExtensions.cs | 6 +- .../CommandResultExtensions.cs | 4 +- .../ParseResultMatchingValueSource.cs | 4 +- .../Directives/DiagramSubsystem.cs | 8 +- src/System.CommandLine.Tests/CommandTests.cs | 24 ++-- .../CustomParsingTests.cs | 18 +-- .../ParseResultTests.cs | 4 +- .../ParserTests.MultiplePositions.cs | 4 +- src/System.CommandLine.Tests/ParserTests.cs | 136 +++++++++--------- .../ParsingValidationTests.cs | 56 ++++---- .../ResponseFileTests.cs | 8 +- src/System.CommandLine/ArgumentArity.cs | 6 +- .../Binding/ArgumentConversionResult.cs | 24 ++-- .../Binding/ArgumentConverter.cs | 22 +-- .../Binding/TryConvertArgument.cs | 2 +- src/System.CommandLine/CliArgument.cs | 4 +- src/System.CommandLine/CliArgument{T}.cs | 4 +- src/System.CommandLine/CliCommand.cs | 4 +- src/System.CommandLine/CliOption{T}.cs | 2 +- .../LocalizationResources.cs | 12 +- src/System.CommandLine/ParseResult.cs | 57 ++++---- ...Result.cs => CliArgumentResultInternal.cs} | 34 ++--- ...mandValueResult.cs => CliCommandResult.cs} | 10 +- ...dResult.cs => CliCommandResultInternal.cs} | 51 +++---- ...onResult.cs => CliOptionResultInternal.cs} | 14 +- .../Parsing/CliSymbolResult.cs | 9 ++ ...olResult.cs => CliSymbolResultInternal.cs} | 16 +-- .../{ValueResult.cs => CliValueResult.cs} | 10 +- .../Parsing/DirectiveResult.cs | 2 +- .../Parsing/ParseDiagramAction.cs | 8 +- src/System.CommandLine/Parsing/ParseError.cs | 8 +- .../Parsing/ParseOperation.cs | 34 ++--- .../Parsing/SymbolLookupByName.cs | 6 +- .../Parsing/SymbolResultExtensions.cs | 4 +- .../Parsing/SymbolResultTree.cs | 32 ++--- .../System.CommandLine.csproj | 14 +- 39 files changed, 341 insertions(+), 330 deletions(-) rename src/System.CommandLine/Parsing/{ArgumentResult.cs => CliArgumentResultInternal.cs} (88%) rename src/System.CommandLine/Parsing/{CommandValueResult.cs => CliCommandResult.cs} (73%) rename src/System.CommandLine/Parsing/{CommandResult.cs => CliCommandResultInternal.cs} (77%) rename src/System.CommandLine/Parsing/{OptionResult.cs => CliOptionResultInternal.cs} (85%) create mode 100644 src/System.CommandLine/Parsing/CliSymbolResult.cs rename src/System.CommandLine/Parsing/{SymbolResult.cs => CliSymbolResultInternal.cs} (89%) rename src/System.CommandLine/Parsing/{ValueResult.cs => CliValueResult.cs} (95%) diff --git a/src/System.CommandLine.Extended/Help/HelpOptionAction.cs b/src/System.CommandLine.Extended/Help/HelpOptionAction.cs index 9700643ae5..fe034647fd 100644 --- a/src/System.CommandLine.Extended/Help/HelpOptionAction.cs +++ b/src/System.CommandLine.Extended/Help/HelpOptionAction.cs @@ -24,7 +24,7 @@ public override int Invoke(ParseResult parseResult) var output = parseResult.Configuration.Output; var helpContext = new HelpContext(Builder, - parseResult.CommandResult.Command, + parseResult.CommandResultInternal.Command, output, parseResult); diff --git a/src/System.CommandLine.Extended/Invocation/ParseErrorAction.cs b/src/System.CommandLine.Extended/Invocation/ParseErrorAction.cs index 5175ad70d7..c85e432757 100644 --- a/src/System.CommandLine.Extended/Invocation/ParseErrorAction.cs +++ b/src/System.CommandLine.Extended/Invocation/ParseErrorAction.cs @@ -66,8 +66,8 @@ private static void WriteHelp(ParseResult parseResult) // Find the most proximate help option (if any) and invoke its action. var availableHelpOptions = parseResult - .CommandResult - .RecurseWhileNotNull(r => r.Parent as CommandResult) + .CommandResultInternal + .RecurseWhileNotNull(r => r.Parent as CommandResultInternal) .Select(r => r.Command.Options.OfType().FirstOrDefault()); if (availableHelpOptions.FirstOrDefault(o => o is not null) is { Action: not null } helpOption) { @@ -92,7 +92,7 @@ private static void WriteTypoCorrectionSuggestions(ParseResult parseResult) var token = unmatchedTokens[i]; bool first = true; - foreach (string suggestion in GetPossibleTokens(parseResult.CommandResult.Command, token)) + foreach (string suggestion in GetPossibleTokens(parseResult.CommandResultInternal.Command, token)) { if (first) { diff --git a/src/System.CommandLine.Extended/VersionOption.cs b/src/System.CommandLine.Extended/VersionOption.cs index 48f56dbc77..53be33155d 100644 --- a/src/System.CommandLine.Extended/VersionOption.cs +++ b/src/System.CommandLine.Extended/VersionOption.cs @@ -47,7 +47,7 @@ private void AddValidators() { Validators.Add(static result => { - if (result.Parent is CommandResult parent && + if (result.Parent is CliCommandResultInternal parent && parent.Children.Any(r => r is not OptionResult { Option: VersionOption })) { result.AddError(LocalizationResources.VersionOptionCannotBeCombinedWithOtherArguments(result.IdentifierToken?.Value ?? result.Option.Name)); diff --git a/src/System.CommandLine.NamingConventionBinder/BindingContextExtensions.cs b/src/System.CommandLine.NamingConventionBinder/BindingContextExtensions.cs index fe707c2347..d1e9ea54a4 100644 --- a/src/System.CommandLine.NamingConventionBinder/BindingContextExtensions.cs +++ b/src/System.CommandLine.NamingConventionBinder/BindingContextExtensions.cs @@ -20,12 +20,12 @@ private sealed class DummyStateHoldingHandler : BindingHandler public static BindingContext GetBindingContext(this ParseResult parseResult) { // parsing resulted with no handler or it was not created yet, we fake it to just store the BindingContext between the calls - if (parseResult.CommandResult.Command.Action is null) + if (parseResult.CommandResultInternal.Command.Action is null) { - parseResult.CommandResult.Command.Action = new DummyStateHoldingHandler(); + parseResult.CommandResultInternal.Command.Action = new DummyStateHoldingHandler(); } - return ((BindingHandler)parseResult.CommandResult.Command.Action).GetBindingContext(parseResult); + return ((BindingHandler)parseResult.CommandResultInternal.Command.Action).GetBindingContext(parseResult); } /// diff --git a/src/System.CommandLine.NamingConventionBinder/CommandResultExtensions.cs b/src/System.CommandLine.NamingConventionBinder/CommandResultExtensions.cs index f20269245e..223056ac29 100644 --- a/src/System.CommandLine.NamingConventionBinder/CommandResultExtensions.cs +++ b/src/System.CommandLine.NamingConventionBinder/CommandResultExtensions.cs @@ -8,7 +8,7 @@ namespace System.CommandLine.NamingConventionBinder; internal static class CommandResultExtensions { internal static bool TryGetValueForArgument( - this CommandResult commandResult, + this CliCommandResultInternal commandResult, IValueDescriptor valueDescriptor, out object? value) { @@ -38,7 +38,7 @@ internal static bool TryGetValueForArgument( } internal static bool TryGetValueForOption( - this CommandResult commandResult, + this CliCommandResultInternal commandResult, IValueDescriptor valueDescriptor, out object? value) { diff --git a/src/System.CommandLine.NamingConventionBinder/ParseResultMatchingValueSource.cs b/src/System.CommandLine.NamingConventionBinder/ParseResultMatchingValueSource.cs index 4168c99deb..190b85318b 100644 --- a/src/System.CommandLine.NamingConventionBinder/ParseResultMatchingValueSource.cs +++ b/src/System.CommandLine.NamingConventionBinder/ParseResultMatchingValueSource.cs @@ -15,7 +15,7 @@ public bool TryGetValue( { if (!string.IsNullOrEmpty(valueDescriptor.ValueName)) { - CommandResult? commandResult = bindingContext?.ParseResult.CommandResult; + CliCommandResultInternal? commandResult = bindingContext?.ParseResult.CommandResultInternal; while (commandResult is { }) { @@ -34,7 +34,7 @@ public bool TryGetValue( return true; } - commandResult = commandResult.Parent as CommandResult; + commandResult = commandResult.Parent as CliCommandResultInternal; } } diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 104d0c3f07..f05627aa1f 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -60,10 +60,10 @@ internal static StringBuilder Diagram(ParseResult parseResult) /* private static void Diagram( StringBuilder builder, - SymbolResult symbolResult, + CliSymbolResultInternal symbolResult, ParseResult parseResult) { - if (parseResult.Errors.Any(e => e.SymbolResult == symbolResult)) + if (parseResult.Errors.Any(e => e.SymbolResultInternal == symbolResult)) { builder.Append('!'); } @@ -155,10 +155,10 @@ private static void Diagram( } else { - builder.Append(((CommandResult)symbolResult).IdentifierToken.Value); + builder.Append(((CliCommandResultInternal)symbolResult).IdentifierToken.Value); } - foreach (SymbolResult child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) + foreach (CliSymbolResultInternal child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) { if (child is ArgumentResult arg && (arg.Argument.ValueType == typeof(bool) || diff --git a/src/System.CommandLine.Tests/CommandTests.cs b/src/System.CommandLine.Tests/CommandTests.cs index 8e2157932d..628068d7ce 100644 --- a/src/System.CommandLine.Tests/CommandTests.cs +++ b/src/System.CommandLine.Tests/CommandTests.cs @@ -42,10 +42,10 @@ public void Outer_command_is_identified_correctly_by_Parent_property() var result = _outerCommand.Parse("outer inner --option argument1"); result - .CommandResult + .CommandResultInternal .Parent .Should() - .BeOfType() + .BeOfType() .Which .Command .Name @@ -58,9 +58,9 @@ public void Inner_command_is_identified_correctly() { var result = _outerCommand.Parse("outer inner --option argument1"); - result.CommandResult + result.CommandResultInternal .Should() - .BeOfType() + .BeOfType() .Which .Command .Name @@ -73,7 +73,7 @@ public void Inner_command_option_is_identified_correctly() { var result = _outerCommand.Parse("outer inner --option argument1"); - result.CommandResult + result.CommandResultInternal .Children .ElementAt(0) .Should() @@ -90,7 +90,7 @@ public void Inner_command_option_argument_is_identified_correctly() { var result = _outerCommand.Parse("outer inner --option argument1"); - result.CommandResult + result.CommandResultInternal .Children .ElementAt(0) .Tokens @@ -114,14 +114,14 @@ public void Commands_at_multiple_levels_can_have_their_own_arguments() var result = outer.Parse("outer arg1 inner arg2 arg3"); - result.CommandResult + result.CommandResultInternal .Parent .Tokens .Select(t => t.Value) .Should() .BeEquivalentTo("arg1"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -200,7 +200,7 @@ public void ParseResult_Command_identifies_innermost_command(string input, strin var result = outer.Parse(input); - result.CommandResult.Command.Name.Should().Be(expectedCommand); + result.CommandResultInternal.Command.Name.Should().Be(expectedCommand); } [Fact] @@ -214,7 +214,7 @@ public void Commands_can_have_aliases() var result = command.Parse("that"); - result.CommandResult.Command.Should().BeSameAs(command); + result.CommandResultInternal.Command.Should().BeSameAs(command); result.Errors.Should().BeEmpty(); } @@ -228,7 +228,7 @@ public void RootCommand_can_have_aliases() var result = command.Parse("that"); - result.CommandResult.Command.Should().BeSameAs(command); + result.CommandResultInternal.Command.Should().BeSameAs(command); result.Errors.Should().BeEmpty(); } @@ -245,7 +245,7 @@ public void Subcommands_can_have_aliases() var result = rootCommand.Parse("that"); - result.CommandResult.Command.Should().BeSameAs(subcommand); + result.CommandResultInternal.Command.Should().BeSameAs(subcommand); result.Errors.Should().BeEmpty(); } diff --git a/src/System.CommandLine.Tests/CustomParsingTests.cs b/src/System.CommandLine.Tests/CustomParsingTests.cs index e29c5ba32e..b1fe4223e1 100644 --- a/src/System.CommandLine.Tests/CustomParsingTests.cs +++ b/src/System.CommandLine.Tests/CustomParsingTests.cs @@ -95,7 +95,7 @@ public void Validation_failure_message_can_be_specified_when_parsing_tokens() new CliRootCommand { argument }.Parse("x") .Errors .Should() - .ContainSingle(e => ((ArgumentResult)e.SymbolResult).Argument == argument) + .ContainSingle(e => ((ArgumentResult)e.SymbolResultInternal).Argument == argument) .Which .Message .Should() @@ -117,7 +117,7 @@ public void Validation_failure_message_can_be_specified_when_evaluating_default_ new CliRootCommand { argument }.Parse("") .Errors .Should() - .ContainSingle(e => ((ArgumentResult)e.SymbolResult).Argument == argument) + .ContainSingle(e => ((ArgumentResult)e.SymbolResultInternal).Argument == argument) .Which .Message .Should() @@ -256,7 +256,7 @@ public void Option_ArgumentResult_parentage_to_root_symbol_is_set_correctly_when .Parent .Parent .Should() - .BeOfType() + .BeOfType() .Which .Command .Should() @@ -268,7 +268,7 @@ public void Option_ArgumentResult_parentage_to_root_symbol_is_set_correctly_when [InlineData("-y value-y -x value-x")] public void Symbol_can_be_found_without_explicitly_traversing_result_tree(string commandLine) { - SymbolResult resultForOptionX = null; + CliSymbolResultInternal resultForOptionX = null; var optionX = new CliOption("-x") { CustomParser = _ => string.Empty @@ -322,7 +322,7 @@ public void Command_ArgumentResult_Parent_is_set_correctly_when_token_is_implici argumentResult .Parent .Should() - .BeOfType() + .BeOfType() .Which .Command .Should() @@ -447,7 +447,7 @@ public void Custom_parser_can_check_another_option_result_for_custom_errors(stri var parseResult = command.Parse(commandLine); parseResult.Errors - .Single(e => e.SymbolResult is OptionResult optResult && + .Single(e => e.SymbolResultInternal is OptionResult optResult && optResult.Option == optionThatDependsOnOptionWithError) .Message .Should() @@ -482,8 +482,8 @@ public void Validation_reports_all_parse_errors() OptionResult secondOptionResult = parseResult.GetResult(secondOptionWithError); secondOptionResult.Errors.Single().Message.Should().Be("second error"); - parseResult.Errors.Should().Contain(error => error.SymbolResult == firstOptionResult); - parseResult.Errors.Should().Contain(error => error.SymbolResult == secondOptionResult); + parseResult.Errors.Should().Contain(error => error.SymbolResultInternal == firstOptionResult); + parseResult.Errors.Should().Contain(error => error.SymbolResultInternal == secondOptionResult); } [Fact] @@ -504,7 +504,7 @@ public void When_custom_conversion_fails_then_an_option_does_not_accept_further_ var result = command.Parse("the-command -x nope yep"); - result.CommandResult.Tokens.Count.Should().Be(1); + result.CommandResultInternal.Tokens.Count.Should().Be(1); } [Fact] diff --git a/src/System.CommandLine.Tests/ParseResultTests.cs b/src/System.CommandLine.Tests/ParseResultTests.cs index b8f9948e93..207e6d732d 100644 --- a/src/System.CommandLine.Tests/ParseResultTests.cs +++ b/src/System.CommandLine.Tests/ParseResultTests.cs @@ -94,12 +94,12 @@ public void Command_will_not_accept_a_command_if_a_sibling_command_has_already_b var result = CliParser.Parse(command, "outer inner-one inner-two"); - result.CommandResult.Command.Name.Should().Be("inner-one"); + result.CommandResultInternal.Command.Name.Should().Be("inner-one"); result.Errors.Count.Should().Be(1); var result2 = CliParser.Parse(command, "outer inner-two inner-one"); - result2.CommandResult.Command.Name.Should().Be("inner-two"); + result2.CommandResultInternal.Command.Name.Should().Be("inner-two"); result2.Errors.Count.Should().Be(1); } diff --git a/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs b/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs index 7c74880137..8480bb967f 100644 --- a/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs +++ b/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs @@ -136,10 +136,10 @@ public void A_command_can_be_specified_in_more_than_one_position( var result = outer.Parse(commandLine); result.Errors.Should().BeEmpty(); - result.CommandResult + result.CommandResultInternal .Parent .Should() - .BeOfType() + .BeOfType() .Which .Command .Name diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 98841082a2..b832c80503 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -139,9 +139,9 @@ public void Option_short_forms_can_be_bundled() var result = CliParser.Parse(command, "the-command -xyz"); - result.CommandValueResult + result.CommandResult .ValueResults - .Select(o => o.ValueSymbol.Name) + .Select(o => ((OptionResult)o).Option.Name) .Should() .BeEquivalentTo("-x", "-y", "-z"); } @@ -189,9 +189,9 @@ public void Option_long_forms_do_not_get_unbundled() var result = CliParser.Parse(command, "the-command --xyz"); - result.CommandResult + result.CommandResultInternal .Children - .Select(o => ((OptionResult)o).Option.Name) + .Select(o => ((CliOptionResultInternal)o).Option.Name) .Should() .BeEquivalentTo("--xyz"); } @@ -211,7 +211,7 @@ public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_v ParseResult result = CliParser.Parse(outer, "outer inner -abc"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -425,7 +425,7 @@ public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_foll .Should() .BeEquivalentTo("carrot"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -443,17 +443,17 @@ public void Command_with_multiple_options_is_parsed_correctly() var result = CliParser.Parse(command, "outer --inner1 argument1 --inner2 argument2"); - result.CommandResult + result.CommandResultInternal .Children .Should() .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner1" && + ((CliOptionResultInternal)o).Option.Name == "--inner1" && o.Tokens.Single().Value == "argument1"); - result.CommandResult + result.CommandResultInternal .Children .Should() .ContainSingle(o => - ((OptionResult)o).Option.Name == "--inner2" && + ((CliOptionResultInternal)o).Option.Name == "--inner2" && o.Tokens.Single().Value == "argument2"); } @@ -576,13 +576,13 @@ public void When_nested_commands_all_accept_arguments_then_the_nearest_captures_ var result = CliParser.Parse(command, "outer arg1 inner arg2"); - result.CommandResult + result.CommandResultInternal .Parent .Tokens.Select(t => t.Value) .Should() .BeEquivalentTo("arg1"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -631,7 +631,7 @@ public void When_child_option_will_not_accept_arg_then_parent_can() var optionResult = result.GetResult(option); optionResult.Tokens.Should().BeEmpty(); - result.CommandResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); + result.CommandResultInternal.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); } [Fact] @@ -646,7 +646,7 @@ public void When_parent_option_will_not_accept_arg_then_child_can() var result = CliParser.Parse(command, "the-command -x the-argument"); result.GetResult(option).Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-argument"); - result.CommandResult.Tokens.Should().BeEmpty(); + result.CommandResultInternal.Tokens.Should().BeEmpty(); } [Fact] @@ -698,18 +698,18 @@ public void When_options_with_the_same_name_are_defined_on_parent_and_child_comm ParseResult result = CliParser.Parse(outer, "outer inner -x"); - result.CommandResult + result.CommandResultInternal .Parent .Should() - .BeOfType() + .BeOfType() .Which .Children .Should() - .AllBeAssignableTo(); - result.CommandResult + .AllBeAssignableTo(); + result.CommandResultInternal .Children .Should() - .ContainSingle(o => ((OptionResult)o).Option.Name == "-x"); + .ContainSingle(o => ((CliOptionResultInternal)o).Option.Name == "-x"); } [Fact] @@ -723,18 +723,18 @@ public void When_options_with_the_same_name_are_defined_on_parent_and_child_comm var result = CliParser.Parse(outer, "outer -x inner"); - result.CommandResult + result.CommandResultInternal .Children .Should() .BeEmpty(); - result.CommandResult + result.CommandResultInternal .Parent .Should() - .BeOfType() + .BeOfType() .Which .Children .Should() - .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); + .ContainSingle(o => o is CliOptionResultInternal && ((CliOptionResultInternal)o).Option.Name == "-x"); } /* @@ -754,12 +754,12 @@ public void Arguments_only_apply_to_the_nearest_command() ParseResult result = outer.Parse("outer inner arg1 arg2"); - result.CommandResult + result.CommandResultInternal .Parent .Tokens .Should() .BeEmpty(); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -814,7 +814,7 @@ public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_a "the-command" }); - CommandResult completeResult = result.CommandResult; + CliCommandResultInternal completeResult = result.CommandResultInternal; completeResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-command"); } @@ -832,7 +832,7 @@ public void Absolute_unix_style_paths_are_lexed_correctly() var result = CliParser.Parse(command, commandText); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -852,7 +852,7 @@ public void Absolute_Windows_style_paths_are_lexed_correctly() ParseResult result = CliParser.Parse(command, commandText); - result.CommandResult + result.CommandResultInternal .Tokens .Should() .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); @@ -996,7 +996,7 @@ public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_t ParseResult result = CliParser.Parse(outer, "outer inner -p:RandomThing=random"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -1042,32 +1042,32 @@ public void Option_and_Command_can_have_the_same_alias() }; CliParser.Parse(outerCommand, "outer inner") - .CommandResult + .CommandResultInternal .Command .Should() .BeSameAs(innerCommand); CliParser.Parse(outerCommand, "outer --inner") - .CommandResult + .CommandResultInternal .Command .Should() .BeSameAs(outerCommand); CliParser.Parse(outerCommand, "outer --inner inner") - .CommandResult + .CommandResultInternal .Command .Should() .BeSameAs(innerCommand); CliParser.Parse(outerCommand, "outer --inner inner") - .CommandResult + .CommandResultInternal .Parent .Should() - .BeOfType() + .BeOfType() .Which .Children .Should() - .Contain(o => ((OptionResult)o).Option == option); + .Contain(o => ((CliOptionResultInternal)o).Option == option); } [Fact] @@ -1082,14 +1082,14 @@ public void Options_can_have_the_same_alias_differentiated_only_by_prefix() option2 }; - CliParser.Parse(rootCommand, "-a").CommandResult + CliParser.Parse(rootCommand, "-a").CommandResultInternal .Children - .Select(s => ((OptionResult)s).Option) + .Select(s => ((CliOptionResultInternal)s).Option) .Should() .BeEquivalentTo(option1); - CliParser.Parse(rootCommand, "--a").CommandResult + CliParser.Parse(rootCommand, "--a").CommandResultInternal .Children - .Select(s => ((OptionResult)s).Option) + .Select(s => ((CliOptionResultInternal)s).Option) .Should() .BeEquivalentTo(option2); } @@ -1196,7 +1196,7 @@ public void Option_arguments_can_match_subcommands() var result = CliParser.Parse(rootCommand, "-a subcommand"); GetValue(result, optionA).Should().Be("subcommand"); - result.CommandResult.Command.Should().BeSameAs(rootCommand); + result.CommandResultInternal.Command.Should().BeSameAs(rootCommand); } [Fact] @@ -1214,7 +1214,7 @@ public void Arguments_can_match_subcommands() var result = CliParser.Parse(rootCommand, "subcommand one two three subcommand four"); - result.CommandResult.Command.Should().BeSameAs(subcommand); + result.CommandResultInternal.Command.Should().BeSameAs(subcommand); GetValue(result, argument) .Should() @@ -1406,12 +1406,12 @@ public void When_a_command_line_has_unmatched_tokens_the_parse_result_action_sho if (treatUnmatchedTokensAsErrors) { result.Errors.Should().NotBeEmpty(); - result.Action.Should().NotBeSameAs(result.CommandResult.Command.Action); + result.Action.Should().NotBeSameAs(result.CommandResultInternal.Command.Action); } else { result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + result.Action.Should().BeSameAs(result.CommandResultInternal.Command.Action); } } @@ -1435,7 +1435,7 @@ public void RootCommand_TreatUnmatchedTokensAsErrors_set_to_false_has_precedence result.UnmatchedTokens.Should().BeEquivalentTo("test1.dll", "test2.dll"); result.Errors.Should().BeEmpty(); - result.Action.Should().BeSameAs(result.CommandResult.Command.Action); + result.Action.Should().BeSameAs(result.CommandResultInternal.Command.Action); } */ @@ -1460,7 +1460,7 @@ public void Command_argument_arity_can_be_a_fixed_value_greater_than_1() }; CliParser.Parse(command, "1 2 3") - .CommandResult + .CommandResultInternal .Tokens .Should() .BeEquivalentTo( @@ -1482,7 +1482,7 @@ public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_tha }; CliParser.Parse(command, "1 2 3") - .CommandResult + .CommandResultInternal .Tokens .Should() .BeEquivalentTo( @@ -1490,7 +1490,7 @@ public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_tha new CliToken("2", CliTokenType.Argument, argument, dummyLocation), new CliToken("3", CliTokenType.Argument, argument, dummyLocation)); CliParser.Parse(command, "1 2 3 4 5") - .CommandResult + .CommandResultInternal .Tokens .Should() .BeEquivalentTo( @@ -1710,11 +1710,11 @@ public void CommandResult_contains_argument_ValueResults() var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandValueResult = parseResult.CommandValueResult; - commandValueResult.ValueResults.Should().HaveCount(2); - var result1 = commandValueResult.ValueResults.First(); + var commandResult = parseResult.CommandResult; + commandResult.ValueResults.Should().HaveCount(2); + var result1 = commandResult.ValueResults[0]; result1.GetValue().Should().Be("Kirk"); - var result2 = commandValueResult.ValueResults.Skip(1).First(); + var result2 = commandValueResult.ValueResults[1]; result2.GetValue().Should().Be("Spock"); } @@ -1735,11 +1735,11 @@ public void CommandResult_contains_option_ValueResults() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); - var commandValueResult = parseResult.CommandValueResult; - commandValueResult.ValueResults.Should().HaveCount(2); - var result1 = commandValueResult.ValueResults[0]; + var commandResult = parseResult.CommandResult; + commandResult.ValueResults.Should().HaveCount(2); + var result1 = commandResult.ValueResults[0]; result1.GetValue().Should().Be("Kirk"); - var result2 = commandValueResult.ValueResults[1]; + var result2 = commandResult.ValueResults[1]; result2.GetValue().Should().Be("Spock"); } @@ -1763,9 +1763,9 @@ public void Location_in_ValueResult_correct_for_arguments() var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandValueResult = parseResult.CommandValueResult; - var result1 = commandValueResult.ValueResults[0]; - var result2 = commandValueResult.ValueResults[1]; + var commandResult = parseResult.CommandResult; + var result1 = commandResult.ValueResults[0]; + var result2 = commandResult.ValueResults[1]; result1.Locations.Single().Should().Be(expectedLocation1); result2.Locations.Single().Should().Be(expectedLocation2); } @@ -1790,9 +1790,9 @@ public void Location_in_ValueResult_correct_for_options() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt2 Spock"); - var commandValueResult = parseResult.CommandValueResult; - var result1 = commandValueResult.ValueResults[0]; - var result2 = commandValueResult.ValueResults[1]; + var commandResult = parseResult.CommandResult; + var result1 = commandResult.ValueResults[0]; + var result2 = commandResult.ValueResults[1]; result1.Locations.Single().Should().Be(expectedLocation1); result2.Locations.Single().Should().Be(expectedLocation2); } @@ -1817,8 +1817,8 @@ public void Location_offsets_in_ValueResult_correct_for_arguments() var parseResult = CliParser.Parse(rootCommand, "subcommand Kirk Spock"); - var commandValueResult = parseResult.CommandValueResult; - var result1 = commandValueResult.ValueResults.Single(); + var commandResult = parseResult.CommandResult; + var result1 = commandResult.ValueResults.Single(); result1.Locations.First().Should().Be(expectedLocation1); result1.Locations.Skip(1).Single().Should().Be(expectedLocation2); } @@ -1841,8 +1841,8 @@ public void Location_offsets_in_ValueResult_correct_for_options() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1 Kirk --opt1 Spock"); - var commandValueResult = parseResult.CommandValueResult; - var result1 = commandValueResult.ValueResults.Single(); + var commandResult = parseResult.CommandResult; + var result1 = commandResult.ValueResults.Single(); result1.Locations.First().Should().Be(expectedLocation1); result1.Locations.Skip(1).Single().Should().Be(expectedLocation2); } @@ -1867,9 +1867,9 @@ public void Location_offset_correct_when_colon_or_equal_used() var parseResult = CliParser.Parse(rootCommand, "subcommand arg1 --opt1:Kirk --opt11=Spock"); - var commandValueResult = parseResult.CommandValueResult; - var result1 = commandValueResult.ValueResults[0]; - var result2 = commandValueResult.ValueResults[1]; + var commandResult = parseResult.CommandResult; + var result1 = commandResult.ValueResults[0]; + var result2 = commandResult.ValueResults[1]; result1.Locations.Single().Should().Be(expectedLocation1); result2.Locations.Single().Should().Be(expectedLocation2); } diff --git a/src/System.CommandLine.Tests/ParsingValidationTests.cs b/src/System.CommandLine.Tests/ParsingValidationTests.cs index bd9687b2d2..f9bf9e5281 100644 --- a/src/System.CommandLine.Tests/ParsingValidationTests.cs +++ b/src/System.CommandLine.Tests/ParsingValidationTests.cs @@ -46,9 +46,9 @@ public void When_an_option_has_en_error_then_the_error_has_a_reference_to_the_op var result = new CliRootCommand { option }.Parse("-x something_else"); result.Errors - .Where(e => e.SymbolResult != null) + .Where(e => e.SymbolResultInternal != null) .Should() - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == option.Name); + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == option.Name); } [Fact] // https://github.com/dotnet/command-line-api/issues/1475 @@ -67,7 +67,7 @@ public void When_FromAmong_is_used_then_the_OptionResult_ErrorMessage_is_set() .Should() .Be(LocalizationResources.UnrecognizedArgument("c", new []{ "a", "b"})); error - .SymbolResult + .SymbolResultInternal .Should() .BeOfType(); @@ -90,7 +90,7 @@ public void When_FromAmong_is_used_then_the_ArgumentResult_ErrorMessage_is_set() .Should() .Be(LocalizationResources.UnrecognizedArgument("c", new []{ "a", "b"})); error - .SymbolResult + .SymbolResultInternal .Should() .BeOfType(); } @@ -380,7 +380,7 @@ public void A_custom_validator_can_be_added_to_an_option() .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option == option) + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option == option) .Which .Message .Should() @@ -407,7 +407,7 @@ public void A_custom_validator_can_be_added_to_an_argument() .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument == argument) + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument == argument) .Which .Message .Should() @@ -476,7 +476,7 @@ public void Validators_on_global_options_are_executed_when_invoking_a_subcommand .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option == option) + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option == option) .Which .Message .Should() @@ -613,7 +613,7 @@ public void LegalFilePathsOnly_rejects_command_arguments_containing_invalid_path .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument == command.Arguments.First() && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument == command.Arguments.First() && e.Message == $"Character not allowed in a path: '{invalidCharacter}'."); } @@ -635,7 +635,7 @@ public void LegalFilePathsOnly_rejects_option_arguments_containing_invalid_path_ .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "-x" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "-x" && e.Message == $"Character not allowed in a path: '{invalidCharacter}'."); } @@ -698,7 +698,7 @@ public void LegalFileNamesOnly_rejects_command_arguments_containing_invalid_file .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument == command.Arguments.First() && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument == command.Arguments.First() && e.Message == $"Character not allowed in a file name: '{invalidCharacter}'."); } @@ -721,7 +721,7 @@ public void LegalFileNamesOnly_rejects_option_arguments_containing_invalid_file_ .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "-x" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "-x" && e.Message == $"Character not allowed in a file name: '{invalidCharacter}'."); } @@ -781,7 +781,7 @@ public void A_command_argument_can_be_invalid_based_on_file_existence() .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"File does not exist: '{path}'."); } @@ -800,7 +800,7 @@ public void An_option_argument_can_be_invalid_based_on_file_existence() .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"File does not exist: '{path}'."); } @@ -819,7 +819,7 @@ public void A_command_argument_can_be_invalid_based_on_directory_existence() .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"Directory does not exist: '{path}'."); } @@ -838,7 +838,7 @@ public void An_option_argument_can_be_invalid_based_on_directory_existence() .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"Directory does not exist: '{path}'."); } @@ -857,7 +857,7 @@ public void A_command_argument_can_be_invalid_based_on_file_or_directory_existen .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument == command.Arguments.First() && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument == command.Arguments.First() && e.Message == $"File or directory does not exist: '{path}'."); } @@ -876,7 +876,7 @@ public void An_option_argument_can_be_invalid_based_on_file_or_directory_existen .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"File or directory does not exist: '{path}'."); } @@ -895,7 +895,7 @@ public void A_command_argument_with_multiple_files_can_be_invalid_based_on_file_ .Should() .HaveCount(1) .And - .Contain(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .Contain(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"File does not exist: '{path}'."); } @@ -914,7 +914,7 @@ public void An_option_argument_with_multiple_files_can_be_invalid_based_on_file_ .Should() .HaveCount(1) .And - .Contain(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .Contain(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"File does not exist: '{path}'."); } @@ -933,7 +933,7 @@ public void A_command_argument_with_multiple_directories_can_be_invalid_based_on .Should() .HaveCount(1) .And - .ContainSingle(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .ContainSingle(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"Directory does not exist: '{path}'."); } @@ -952,7 +952,7 @@ public void An_option_argument_with_multiple_directories_can_be_invalid_based_on .Should() .HaveCount(1) .And - .ContainSingle(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .ContainSingle(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"Directory does not exist: '{path}'."); } @@ -973,7 +973,7 @@ public void A_command_argument_with_multiple_FileSystemInfos_can_be_invalid_base result.Errors .Should() - .ContainSingle(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .ContainSingle(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"File or directory does not exist: '{path}'."); } @@ -992,7 +992,7 @@ public void An_option_argument_with_multiple_FileSystemInfos_can_be_invalid_base result.Errors .Should() - .ContainSingle(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .ContainSingle(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"File or directory does not exist: '{path}'."); } @@ -1011,7 +1011,7 @@ public void A_command_argument_with_multiple_FileSystemInfos_can_be_invalid_base .Should() .HaveCount(1) .And - .ContainSingle(e => ((ArgumentResult)e.SymbolResult).Argument.Name == "to" && + .ContainSingle(e => ((ArgumentResult)e.SymbolResultInternal).Argument.Name == "to" && e.Message == $"File or directory does not exist: '{path}'."); } @@ -1030,7 +1030,7 @@ public void An_option_argument_with_multiple_FileSystemInfos_can_be_invalid_base .Should() .HaveCount(1) .And - .ContainSingle(e => ((OptionResult)e.SymbolResult).Option.Name == "--to" && + .ContainSingle(e => ((OptionResult)e.SymbolResultInternal).Option.Name == "--to" && e.Message == $"File or directory does not exist: '{path}'."); } @@ -1123,7 +1123,7 @@ public void A_command_with_subcommands_is_invalid_to_invoke_if_it_has_no_handler .Should() .ContainSingle( e => e.Message.Equals(LocalizationResources.RequiredCommandWasNotProvided()) && - ((CommandResult)e.SymbolResult).Command.Name.Equals("inner")); + ((CliCommandResultInternal)e.SymbolResultInternal).Command.Name.Equals("inner")); } [Fact] @@ -1139,7 +1139,7 @@ public void A_root_command_with_subcommands_is_invalid_to_invoke_if_it_has_no_ha .Should() .ContainSingle( e => e.Message.Equals(LocalizationResources.RequiredCommandWasNotProvided()) && - ((CommandResult)e.SymbolResult).Command == rootCommand); + ((CliCommandResultInternal)e.SymbolResultInternal).Command == rootCommand); } [Fact] @@ -1155,7 +1155,7 @@ public void A_command_with_subcommands_is_valid_to_invoke_if_it_has_a_handler() var result = outer.Parse("outer inner"); result.Errors.Should().BeEmpty(); - result.CommandResult.Command.Should().BeSameAs(inner); + result.CommandResultInternal.Command.Should().BeSameAs(inner); } [Fact] diff --git a/src/System.CommandLine.Tests/ResponseFileTests.cs b/src/System.CommandLine.Tests/ResponseFileTests.cs index 886116ea98..c3725ba443 100644 --- a/src/System.CommandLine.Tests/ResponseFileTests.cs +++ b/src/System.CommandLine.Tests/ResponseFileTests.cs @@ -87,7 +87,7 @@ public void When_response_file_is_specified_it_loads_command_arguments_from_resp } .Parse($"@{responseFile}"); - result.CommandResult + result.CliCommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -111,7 +111,7 @@ public void Response_file_can_provide_subcommand_arguments() } .Parse($"subcommand @{responseFile}"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -132,7 +132,7 @@ public void Response_file_can_provide_subcommand() } .Parse($"@{responseFile} one two three"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -156,7 +156,7 @@ public void When_response_file_is_specified_it_loads_subcommand_arguments_from_r } .Parse($"subcommand @{responseFile}"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() diff --git a/src/System.CommandLine/ArgumentArity.cs b/src/System.CommandLine/ArgumentArity.cs index efebad3713..6a96acdaf9 100644 --- a/src/System.CommandLine/ArgumentArity.cs +++ b/src/System.CommandLine/ArgumentArity.cs @@ -73,11 +73,11 @@ public bool Equals(ArgumentArity other) => public override int GetHashCode() => MaximumNumberOfValues ^ MinimumNumberOfValues ^ IsNonDefault.GetHashCode(); - internal static bool Validate(ArgumentResult argumentResult, [NotNullWhen(false)] out ArgumentConversionResult? error) + internal static bool Validate(CliArgumentResultInternal argumentResult, [NotNullWhen(false)] out ArgumentConversionResult? error) { error = null; - if (argumentResult.Parent is null or OptionResult { Implicit: true }) + if (argumentResult.Parent is null or CliOptionResultInternal { Implicit: true }) { return true; } @@ -95,7 +95,7 @@ internal static bool Validate(ArgumentResult argumentResult, [NotNullWhen(false) if (tokenCount > argumentResult.Argument.Arity.MaximumNumberOfValues) { - if (argumentResult.Parent is OptionResult optionResult) + if (argumentResult.Parent is CliOptionResultInternal optionResult) { if (!optionResult.Option.AllowMultipleArgumentsPerToken) { diff --git a/src/System.CommandLine/Binding/ArgumentConversionResult.cs b/src/System.CommandLine/Binding/ArgumentConversionResult.cs index 8c151c6d5d..7e03e86034 100644 --- a/src/System.CommandLine/Binding/ArgumentConversionResult.cs +++ b/src/System.CommandLine/Binding/ArgumentConversionResult.cs @@ -8,43 +8,43 @@ namespace System.CommandLine.Binding { internal sealed class ArgumentConversionResult { - internal readonly ArgumentResult ArgumentResult; + internal readonly CliArgumentResultInternal ArgumentResultInternal; internal readonly object? Value; internal readonly string? ErrorMessage; internal ArgumentConversionResultType Result; - private ArgumentConversionResult(ArgumentResult argumentResult, string error, ArgumentConversionResultType failure) + private ArgumentConversionResult(CliArgumentResultInternal argumentResult, string error, ArgumentConversionResultType failure) { - ArgumentResult = argumentResult; + ArgumentResultInternal = argumentResult; ErrorMessage = error; Result = failure; } - private ArgumentConversionResult(ArgumentResult argumentResult, object? value, ArgumentConversionResultType result) + private ArgumentConversionResult(CliArgumentResultInternal argumentResult, object? value, ArgumentConversionResultType result) { - ArgumentResult = argumentResult; + ArgumentResultInternal = argumentResult; Value = value; Result = result; } - internal static ArgumentConversionResult Failure(ArgumentResult argumentResult, string error, ArgumentConversionResultType reason) + internal static ArgumentConversionResult Failure(CliArgumentResultInternal argumentResult, string error, ArgumentConversionResultType reason) => new(argumentResult, error, reason); - internal static ArgumentConversionResult ArgumentConversionCannotParse(ArgumentResult argumentResult, Type expectedType, string value) + internal static ArgumentConversionResult ArgumentConversionCannotParse(CliArgumentResultInternal argumentResult, Type expectedType, string value) => new(argumentResult, FormatErrorMessage(argumentResult, expectedType, value), ArgumentConversionResultType.FailedType); - public static ArgumentConversionResult Success(ArgumentResult argumentResult, object? value) + public static ArgumentConversionResult Success(CliArgumentResultInternal argumentResult, object? value) => new(argumentResult, value, ArgumentConversionResultType.Successful); - internal static ArgumentConversionResult None(ArgumentResult argumentResult) + internal static ArgumentConversionResult None(CliArgumentResultInternal argumentResult) => new(argumentResult, value: null, ArgumentConversionResultType.NoArgument); private static string FormatErrorMessage( - ArgumentResult argumentResult, + CliArgumentResultInternal argumentResult, Type expectedType, string value) { - if (argumentResult.Parent is CommandResult commandResult) + if (argumentResult.Parent is CliCommandResultInternal commandResult) { string alias = commandResult.Command.Name; // TODO: completion @@ -62,7 +62,7 @@ private static string FormatErrorMessage( return LocalizationResources.ArgumentConversionCannotParseForCommand(value, alias, expectedType); } } - else if (argumentResult.Parent is OptionResult optionResult) + else if (argumentResult.Parent is CliOptionResultInternal optionResult) { string alias = optionResult.Option.Name; // TODO: completion diff --git a/src/System.CommandLine/Binding/ArgumentConverter.cs b/src/System.CommandLine/Binding/ArgumentConverter.cs index 9fa921348a..e70a0110d9 100644 --- a/src/System.CommandLine/Binding/ArgumentConverter.cs +++ b/src/System.CommandLine/Binding/ArgumentConverter.cs @@ -10,7 +10,7 @@ namespace System.CommandLine.Binding internal static partial class ArgumentConverter { internal static ArgumentConversionResult ConvertObject( - ArgumentResult argumentResult, + CliArgumentResultInternal argumentResult, Type type, object? value) { @@ -36,7 +36,7 @@ internal static ArgumentConversionResult ConvertObject( } private static ArgumentConversionResult ConvertToken( - ArgumentResult argumentResult, + CliArgumentResultInternal argumentResult, Type type, CliToken token) { @@ -81,7 +81,7 @@ private static ArgumentConversionResult ConvertToken( } private static ArgumentConversionResult ConvertTokens( - ArgumentResult argumentResult, + CliArgumentResultInternal argumentResult, Type type, IReadOnlyList tokens) { @@ -110,7 +110,7 @@ private static ArgumentConversionResult ConvertTokens( break; default: // failures - if (argumentResult.Parent is CommandResult) + if (argumentResult.Parent is CliCommandResultInternal) { argumentResult.OnlyTake(i); @@ -132,15 +132,15 @@ private static ArgumentConversionResult ConvertTokens( if (argument.ValueType.TryGetNullableType(out var nullableType) && StringConverters.TryGetValue(nullableType, out var convertNullable)) { - return (ArgumentResult result, out object? value) => ConvertSingleString(result, convertNullable, out value); + return (CliArgumentResultInternal result, out object? value) => ConvertSingleString(result, convertNullable, out value); } if (StringConverters.TryGetValue(argument.ValueType, out var convert1)) { - return (ArgumentResult result, out object? value) => ConvertSingleString(result, convert1, out value); + return (CliArgumentResultInternal result, out object? value) => ConvertSingleString(result, convert1, out value); } - static bool ConvertSingleString(ArgumentResult result, TryConvertString convert, out object? value) => + static bool ConvertSingleString(CliArgumentResultInternal result, TryConvertString convert, out object? value) => convert(result.Tokens[result.Tokens.Count - 1].Value, out value); } @@ -183,12 +183,12 @@ internal static ArgumentConversionResult ConvertIfNeeded( return conversionResult.Result switch { ArgumentConversionResultType.Successful when !toType.IsInstanceOfType(conversionResult.Value) => - ConvertObject(conversionResult.ArgumentResult, + ConvertObject(conversionResult.ArgumentResultInternal, toType, conversionResult.Value), - ArgumentConversionResultType.NoArgument when conversionResult.ArgumentResult.Argument.IsBoolean() => - Success(conversionResult.ArgumentResult, true), + ArgumentConversionResultType.NoArgument when conversionResult.ArgumentResultInternal.Argument.IsBoolean() => + Success(conversionResult.ArgumentResultInternal, true), _ => conversionResult }; @@ -204,7 +204,7 @@ internal static T GetValueOrDefault(this ArgumentConversionResult result) }; } - public static bool TryConvertArgument(ArgumentResult argumentResult, out object? value) + public static bool TryConvertArgument(CliArgumentResultInternal argumentResult, out object? value) { var argument = argumentResult.Argument; diff --git a/src/System.CommandLine/Binding/TryConvertArgument.cs b/src/System.CommandLine/Binding/TryConvertArgument.cs index 44d777e939..f8fb30d9f4 100644 --- a/src/System.CommandLine/Binding/TryConvertArgument.cs +++ b/src/System.CommandLine/Binding/TryConvertArgument.cs @@ -6,6 +6,6 @@ namespace System.CommandLine.Binding { internal delegate bool TryConvertArgument( - ArgumentResult argumentResult, + CliArgumentResultInternal argumentResult, out object? value); } \ No newline at end of file diff --git a/src/System.CommandLine/CliArgument.cs b/src/System.CommandLine/CliArgument.cs index 89c83d6698..34d4e1b228 100644 --- a/src/System.CommandLine/CliArgument.cs +++ b/src/System.CommandLine/CliArgument.cs @@ -110,10 +110,10 @@ public List>> CompletionSour /// Returns the default value for the argument, if defined. Null otherwise. public object? GetDefaultValue() { - return GetDefaultValue(new ArgumentResult(this, null!, null)); + return GetDefaultValue(new CliArgumentResultInternal(this, null!, null)); } - internal abstract object? GetDefaultValue(ArgumentResult argumentResult); + internal abstract object? GetDefaultValue(CliArgumentResultInternal argumentResult); /// /// Specifies if a default value is defined for the argument. diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index 77c31edb7b..d9ac656f1d 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -34,7 +34,7 @@ public CliArgument(string name) : base(name) /// the delegate is also invoked when an input was provided. /// */ - internal Func? DefaultValueFactory { get; set; } + internal Func? DefaultValueFactory { get; set; } // TODO: custom parsers /* @@ -81,7 +81,7 @@ public CliArgument(string name) : base(name) /// public override bool HasDefaultValue => DefaultValueFactory is not null; - internal override object? GetDefaultValue(ArgumentResult argumentResult) + internal override object? GetDefaultValue(CliArgumentResultInternal argumentResult) { if (DefaultValueFactory is null) { diff --git a/src/System.CommandLine/CliCommand.cs b/src/System.CommandLine/CliCommand.cs index b4e01e3909..8adccb5d9d 100644 --- a/src/System.CommandLine/CliCommand.cs +++ b/src/System.CommandLine/CliCommand.cs @@ -30,7 +30,7 @@ public class CliCommand : CliSymbol, IEnumerable private ChildSymbolList? _subcommands; // TODO: validators /* - private List>? _validators; + private List>? _validators; */ /// @@ -91,7 +91,7 @@ public IEnumerable Children /// Validators to the command. Validators can be used /// to create custom validation logic. /// - public List> Validators => _validators ??= new (); + public List> Validators => _validators ??= new (); internal bool HasValidators => _validators is not null && _validators.Count > 0; diff --git a/src/System.CommandLine/CliOption{T}.cs b/src/System.CommandLine/CliOption{T}.cs index 1a233183ad..f2ca874d1a 100644 --- a/src/System.CommandLine/CliOption{T}.cs +++ b/src/System.CommandLine/CliOption{T}.cs @@ -30,7 +30,7 @@ private protected CliOption(string name, string[] aliases, CliArgument argume } /// - internal Func? DefaultValueFactory + internal Func? DefaultValueFactory { get => _argument.DefaultValueFactory; set => _argument.DefaultValueFactory = value; diff --git a/src/System.CommandLine/LocalizationResources.cs b/src/System.CommandLine/LocalizationResources.cs index fc376d7cf4..cfde47ccbd 100644 --- a/src/System.CommandLine/LocalizationResources.cs +++ b/src/System.CommandLine/LocalizationResources.cs @@ -16,7 +16,7 @@ internal static class LocalizationResources /// /// Interpolates values into a localized string similar to Command '{0}' expects a single argument but {1} were provided. /// - internal static string ExpectsOneArgument(OptionResult optionResult) + internal static string ExpectsOneArgument(CliOptionResultInternal optionResult) => GetResourceString(Properties.Resources.OptionExpectsOneArgument, GetOptionName(optionResult), optionResult.Tokens.Count); /* /// @@ -52,15 +52,15 @@ internal static string InvalidCharactersInFileName(char invalidChar) => /// /// Interpolates values into a localized string similar to Required argument missing for command: {0}. /// - internal static string RequiredArgumentMissing(ArgumentResult argumentResult) => - argumentResult.Parent is CommandResult commandResult + internal static string RequiredArgumentMissing(CliArgumentResultInternal argumentResult) => + argumentResult.Parent is CliCommandResultInternal commandResult ? GetResourceString(Properties.Resources.CommandRequiredArgumentMissing, commandResult.IdentifierToken.Value) - : RequiredArgumentMissing((OptionResult)argumentResult.Parent!); + : RequiredArgumentMissing((CliOptionResultInternal)argumentResult.Parent!); /// /// Interpolates values into a localized string similar to Required argument missing for option: {0}. /// - internal static string RequiredArgumentMissing(OptionResult optionResult) => + internal static string RequiredArgumentMissing(CliOptionResultInternal optionResult) => GetResourceString(Properties.Resources.OptionRequiredArgumentMissing, GetOptionName(optionResult)); /// @@ -252,6 +252,6 @@ private static string GetResourceString(string resourceString, params object[] f return resourceString; } - private static string GetOptionName(OptionResult optionResult) => optionResult.IdentifierToken?.Value ?? optionResult.Option.Name; + private static string GetOptionName(CliOptionResultInternal optionResult) => optionResult.IdentifierToken?.Value ?? optionResult.Option.Name; } } diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 2728ef67ff..4471c3bfc3 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -14,11 +14,11 @@ namespace System.CommandLine /// public sealed class ParseResult { - private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); + private readonly IReadOnlyDictionary valueResultDictionary = new Dictionary(); private SymbolLookupByName? symbolLookupByName = null; // TODO: Remove usage and remove - private readonly CommandResult _rootCommandResult; + private readonly CliCommandResultInternal _rootCommandResult; // TODO: unmatched tokens, invocation, completion /* private readonly IReadOnlyList _unmatchedTokens; @@ -30,10 +30,10 @@ public sealed class ParseResult internal ParseResult( CliConfiguration configuration, // TODO: Remove RootCommandResult - it is the root of the CommandValueResult ancestors (fix that) - CommandResult rootCommandResult, + CliCommandResultInternal rootCommandResult, // TODO: Replace with CommandValueResult and remove CommandResult - CommandResult commandResult, - IReadOnlyDictionary valueResultDictionary, + CliCommandResultInternal commandResult, + IReadOnlyDictionary valueResultDictionary, /* List tokens, */ @@ -51,8 +51,9 @@ internal ParseResult( { Configuration = configuration; _rootCommandResult = rootCommandResult; - CommandResult = commandResult; - CommandValueResult = commandResult.CommandValueResult; + // TODO: Why do we need this? + CommandResultInternal = commandResult; + CommandResult = commandResult.CommandValueResult; this.valueResultDictionary = valueResultDictionary; // TODO: invocation /* @@ -102,9 +103,9 @@ internal ParseResult( /// A result indicating the command specified in the command line input. /// // TODO: Update SymbolLookupByName to use CommandValueResult, then remove - internal CommandResult CommandResult { get; } + internal CliCommandResultInternal CommandResultInternal { get; } - public CommandValueResult CommandValueResult { get; } + public CliCommandResult CommandResult { get; } /// /// The configuration used to produce the parse result. @@ -114,8 +115,8 @@ internal ParseResult( /// /// Gets the root command result. /// - /// TODO: Update usage and then remove - internal CommandResult RootCommandResult => _rootCommandResult; + // TODO: Update usage and then remove + internal CliCommandResultInternal RootCommandResult => _rootCommandResult; /// /// Gets the parse errors found while parsing command line input. @@ -206,7 +207,7 @@ CommandLineText is null /// /// The option for which to find a result. /// A result for the specified option, or if it was not entered by the user. - public ValueResult? GetValueResult(CliOption option) + public CliValueResult? GetValueResult(CliOption option) => GetValueResultInternal(option); /// @@ -214,10 +215,10 @@ CommandLineText is null /// /// The argument for which to find a result. /// A result for the specified argument, or if it was not entered by the user. - public ValueResult? GetValueResult(CliArgument argument) + public CliValueResult? GetValueResult(CliArgument argument) => GetValueResultInternal(argument); - private ValueResult? GetValueResultInternal(CliSymbol symbol) + private CliValueResult? GetValueResultInternal(CliSymbol symbol) => valueResultDictionary.TryGetValue(symbol, out var result) ? result : null; @@ -228,7 +229,7 @@ CommandLineText is null /// /// The argument for which to find a result. /// A result for the specified argument, or if it was not provided and no default was configured. - internal ArgumentResult? GetResult(CliArgument argument) => + internal CliArgumentResultInternal? GetResult(CliArgument argument) => _rootCommandResult.GetResult(argument); /* Not used @@ -237,7 +238,7 @@ CommandLineText is null /// /// The command for which to find a result. /// A result for the specified command, or if it was not provided. - internal CommandResult? GetResult(CliCommand command) => + internal CliCommandResultInternal? GetResult(CliCommand command) => _rootCommandResult.GetResult(command); */ @@ -246,7 +247,7 @@ CommandLineText is null /// /// The option for which to find a result. /// A result for the specified option, or if it was not provided and no default was configured. - internal OptionResult? GetResult(CliOption option) => + internal CliOptionResultInternal? GetResult(CliOption option) => _rootCommandResult.GetResult(option); // TODO: Directives @@ -264,8 +265,8 @@ CommandLineText is null /// /// The symbol for which to find a result. /// A result for the specified symbol, or if it was not provided and no default was configured. - public SymbolResult? GetResult(CliSymbol symbol) - => _rootCommandResult.SymbolResultTree.TryGetValue(symbol, out SymbolResult? result) ? result : null; + public CliSymbolResultInternal? GetResult(CliSymbol symbol) + => _rootCommandResult.SymbolResultTree.TryGetValue(symbol, out CliSymbolResultInternal? result) ? result : null; */ // TODO: completion, invocation /* @@ -277,14 +278,14 @@ CommandLineText is null public IEnumerable GetCompletions( int? position = null) { - SymbolResult currentSymbolResult = SymbolToComplete(position); + CliSymbolResultInternal currentSymbolResult = SymbolToComplete(position); CliSymbol currentSymbol = currentSymbolResult switch { ArgumentResult argumentResult => argumentResult.Argument, OptionResult optionResult => optionResult.Option, DirectiveResult directiveResult => directiveResult.Directive, - _ => ((CommandResult)currentSymbolResult).Command + _ => ((CliCommandResultInternal)currentSymbolResult).Command }; var context = GetCompletionContext(); @@ -297,7 +298,7 @@ public IEnumerable GetCompletions( var completions = currentSymbol.GetCompletions(context); - string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult + string[] optionsWithArgumentLimitReached = currentSymbolResult is CliCommandResultInternal commandResult ? OptionsWithArgumentLimitReached(commandResult) : Array.Empty(); @@ -306,7 +307,7 @@ public IEnumerable GetCompletions( return completions; - static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => + static string[] OptionsWithArgumentLimitReached(CliCommandResultInternal commandResult) => commandResult .Children .OfType() @@ -363,13 +364,13 @@ public int Invoke() /// Gets the for parsed result. The handler represents the action /// that will be performed when the parse result is invoked. /// - public CliAction? Action => _action ?? CommandResult.Command.Action; + public CliAction? Action => _action ?? CliCommandResultInternal.Command.Action; internal IReadOnlyList? PreActions => _preActions; - private SymbolResult SymbolToComplete(int? position = null) + private CliSymbolResultInternal SymbolToComplete(int? position = null) { - var commandResult = CommandResult; + var commandResult = CliCommandResultInternal; var allSymbolResultsForCompletion = AllSymbolResultsForCompletion(); @@ -377,11 +378,11 @@ private SymbolResult SymbolToComplete(int? position = null) return currentSymbol; - IEnumerable AllSymbolResultsForCompletion() + IEnumerable AllSymbolResultsForCompletion() { foreach (var item in commandResult.AllSymbolResults()) { - if (item is CommandResult command) + if (item is CliCommandResultInternal command) { yield return command; } diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/CliArgumentResultInternal.cs similarity index 88% rename from src/System.CommandLine/Parsing/ArgumentResult.cs rename to src/System.CommandLine/Parsing/CliArgumentResultInternal.cs index 8dc87d16a7..db525fe4ee 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/CliArgumentResultInternal.cs @@ -9,21 +9,21 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing an . /// - internal sealed class ArgumentResult : SymbolResult + internal sealed class CliArgumentResultInternal : CliSymbolResultInternal { private ArgumentConversionResult? _conversionResult; private bool _onlyTakeHasBeenCalled; - internal ArgumentResult( + internal CliArgumentResultInternal( CliArgument argument, SymbolResultTree symbolResultTree, - SymbolResult? parent) : base(symbolResultTree, parent) + CliSymbolResultInternal? parent) : base(symbolResultTree, parent) { Argument = argument ?? throw new ArgumentNullException(nameof(argument)); } - private ValueResult? _valueResult; - public ValueResult ValueResult + private CliValueResult? _valueResult; + public CliValueResult ValueResult { get { @@ -65,7 +65,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. + /// Thrown if this method is called by Option-owned CliArgumentResultInternal. public void OnlyTake(int numberOfTokens) { if (numberOfTokens < 0) @@ -78,9 +78,9 @@ public void OnlyTake(int numberOfTokens) throw new InvalidOperationException($"{nameof(OnlyTake)} can only be called once."); } - if (Parent is OptionResult) + if (Parent is CliOptionResultInternal) { - throw new NotSupportedException($"{nameof(OnlyTake)} is supported only for a {nameof(CliCommand)}-owned {nameof(ArgumentResult)}"); + throw new NotSupportedException($"{nameof(OnlyTake)} is supported only for a {nameof(CliCommand)}-owned {nameof(CliArgumentResultInternal)}"); } _onlyTakeHasBeenCalled = true; @@ -90,7 +90,7 @@ public void OnlyTake(int numberOfTokens) return; } - CommandResult parent = (CommandResult)Parent!; + CliCommandResultInternal parent = (CliCommandResultInternal)Parent!; var arguments = parent.Command.Arguments; int argumentIndex = arguments.IndexOf(Argument); int nextArgumentIndex = argumentIndex + 1; @@ -99,16 +99,16 @@ public void OnlyTake(int numberOfTokens) while (tokensToPass > 0 && nextArgumentIndex < arguments.Count) { CliArgument nextArgument = parent.Command.Arguments[nextArgumentIndex]; - ArgumentResult nextArgumentResult; + CliArgumentResultInternal nextArgumentResult; - if (SymbolResultTree.TryGetValue(nextArgument, out SymbolResult? symbolResult)) + if (SymbolResultTree.TryGetValue(nextArgument, out CliSymbolResultInternal? symbolResult)) { - nextArgumentResult = (ArgumentResult)symbolResult; + nextArgumentResult = (CliArgumentResultInternal)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); + nextArgumentResult = new CliArgumentResultInternal(nextArgument, SymbolResultTree, Parent); SymbolResultTree.Add(nextArgument, nextArgumentResult); } @@ -123,7 +123,7 @@ public void OnlyTake(int numberOfTokens) nextArgumentIndex++; } - CommandResult rootCommand = parent; + CliCommandResultInternal rootCommand = parent; // When_tokens_are_passed_on_by_custom_parser_on_last_argument_then_they_become_unmatched_tokens while (tokensToPass > 0) { @@ -135,7 +135,7 @@ public void OnlyTake(int numberOfTokens) } /// - public override string ToString() => $"{nameof(ArgumentResult)} {Argument.Name}: {string.Join(" ", Tokens.Select(t => $"<{t.Value}>"))}"; + public override string ToString() => $"{nameof(CliArgumentResultInternal)} {Argument.Name}: {string.Join(" ", Tokens.Select(t => $"<{t.Value}>"))}"; /// internal override void AddError(string errorMessage) @@ -232,8 +232,8 @@ ArgumentConversionResult ReportErrorIfNeeded(ArgumentConversionResult result) /// /// Since Option.Argument is an internal implementation detail, this ArgumentResult applies to the OptionResult in public API if the parent is an OptionResult. /// - private SymbolResult AppliesToPublicSymbolResult => - Parent is OptionResult optionResult ? optionResult : this; + private CliSymbolResultInternal AppliesToPublicSymbolResult => + Parent is CliOptionResultInternal optionResult ? optionResult : this; internal static ValueResultOutcome GetValueResultOutcome(ArgumentConversionResultType? resultType) => resultType switch diff --git a/src/System.CommandLine/Parsing/CommandValueResult.cs b/src/System.CommandLine/Parsing/CliCommandResult.cs similarity index 73% rename from src/System.CommandLine/Parsing/CommandValueResult.cs rename to src/System.CommandLine/Parsing/CliCommandResult.cs index a054f47506..3617df4a3e 100644 --- a/src/System.CommandLine/Parsing/CommandValueResult.cs +++ b/src/System.CommandLine/Parsing/CliCommandResult.cs @@ -9,16 +9,16 @@ namespace System.CommandLine.Parsing; /// Provides the publicly facing command result /// /// -/// The name is temporary as we expect to later name this CommandResult and the previous one to CommandResultInternal +/// The name is temporary as we expect to later name this CliCommandResultInternal and the previous one to CommandResultInternal /// -public class CommandValueResult +public class CliCommandResult : CliSymbolResult { /// /// Creates a CommandValueResult instance /// /// The CliCommand that the result is for. /// The parent command in the case of a CLI hierarchy, or null if there is no parent. - internal CommandValueResult(CliCommand command, CommandValueResult? parent = null) + internal CliCommandResult(CliCommand command, CliCommandResult? parent = null) { Command = command; Parent = parent; @@ -27,7 +27,7 @@ internal CommandValueResult(CliCommand command, CommandValueResult? parent = nul /// /// The ValueResult instances for user entered data. This is a sparse list. /// - public IReadOnlyList ValueResults { get; internal set; } = []; + public IEnumerable ValueResults { get; } = []; /// /// The CliCommand that the result is for. @@ -37,6 +37,6 @@ internal CommandValueResult(CliCommand command, CommandValueResult? parent = nul /// /// The command's parent if one exists, otherwise, null /// - public CommandValueResult? Parent { get; } + public CliCommandResult? Parent { get; } } diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CliCommandResultInternal.cs similarity index 77% rename from src/System.CommandLine/Parsing/CommandResult.cs rename to src/System.CommandLine/Parsing/CliCommandResultInternal.cs index 742d33128c..54172c3832 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CliCommandResultInternal.cs @@ -9,13 +9,14 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing a . /// - internal sealed class CommandResult : SymbolResult + internal sealed class CliCommandResultInternal + : CliSymbolResultInternal { - internal CommandResult( + internal CliCommandResultInternal( CliCommand command, CliToken token, SymbolResultTree symbolResultTree, - CommandResult? parent = null) : + CliCommandResultInternal? parent = null) : base(symbolResultTree, parent) { Command = command ?? throw new ArgumentNullException(nameof(command)); @@ -36,36 +37,36 @@ internal CommandResult( /// /// Child symbol results in the parse tree. /// - public IEnumerable Children => SymbolResultTree.GetChildren(this); + public IEnumerable Children => SymbolResultTree.GetChildren(this); - private CommandValueResult? commandValueResult; - public CommandValueResult CommandValueResult + private CliCommandResult? commandResult; + public CliCommandResult CommandResult { get { - if (commandValueResult is null) + if (commandResult is null) { - var parent = Parent is CommandResult commandResult - ? commandResult.CommandValueResult + var parent = Parent is CliCommandResultInternal commandResultInternal + ? commandResultInternal.CommandValueResult : null; - commandValueResult = new CommandValueResult(Command, parent); + commandResult = new CliCommandResult(Command, parent); } // Reset unless we put tests in place to ensure it is not called in error handling before SymbolTree processing is complete - commandValueResult.ValueResults = Children.Select(GetValueResult).OfType().ToList(); - return commandValueResult; + commandResult.ValueResults = Children.Select(GetValueResult).OfType().ToList(); + return commandResult; } } - private ValueResult? GetValueResult(SymbolResult symbolResult) + private CliValueResult? GetValueResult(CliSymbolResultInternal symbolResult) => symbolResult switch { - ArgumentResult argumentResult => argumentResult.ValueResult, - OptionResult optionResult => optionResult.ValueResult, + CliArgumentResultInternal argumentResult => argumentResult.ValueResult, + CliOptionResultInternal optionResult => optionResult.ValueResult, _ => null! }; /// - public override string ToString() => $"{nameof(CommandResult)}: {IdentifierToken.Value} {string.Join(" ", Tokens.Select(t => t.Value))}"; + public override string ToString() => $"{nameof(CliCommandResultInternal)}: {IdentifierToken.Value} {string.Join(" ", Tokens.Select(t => t.Value))}"; // TODO: DefaultValues /* @@ -128,10 +129,10 @@ private void ValidateOptions(bool completeValidation) continue; } - OptionResult optionResult; - ArgumentResult argumentResult; + CliOptionResultInternal optionResult; + CliArgumentResultInternal argumentResult; - if (!SymbolResultTree.TryGetValue(option, out SymbolResult? symbolResult)) + if (!SymbolResultTree.TryGetValue(option, out CliSymbolResultInternal? symbolResult)) { if (option.Required || option.Argument.HasDefaultValue) { @@ -154,8 +155,8 @@ private void ValidateOptions(bool completeValidation) } else { - optionResult = (OptionResult)symbolResult; - argumentResult = (ArgumentResult)SymbolResultTree[option.Argument]; + optionResult = (CliOptionResultInternal)symbolResult; + argumentResult = (CliArgumentResultInternal)SymbolResultTree[option.Argument]; } // When_there_is_an_arity_error_then_further_errors_are_not_reported @@ -199,14 +200,14 @@ private void ValidateArguments(bool completeValidation) continue; } - ArgumentResult? argumentResult; - if (SymbolResultTree.TryGetValue(argument, out SymbolResult? symbolResult)) + CliArgumentResultInternal? argumentResult; + if (SymbolResultTree.TryGetValue(argument, out CliSymbolResultInternal? symbolResult)) { - argumentResult = (ArgumentResult)symbolResult; + argumentResult = (CliArgumentResultInternal)symbolResult; } else if (argument.HasDefaultValue || argument.Arity.MinimumNumberOfValues > 0) { - argumentResult = new ArgumentResult(argument, SymbolResultTree, this); + argumentResult = new CliArgumentResultInternal(argument, SymbolResultTree, this); SymbolResultTree[argument] = argumentResult; if (!argument.HasDefaultValue && argument.Arity.MinimumNumberOfValues > 0) diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/CliOptionResultInternal.cs similarity index 85% rename from src/System.CommandLine/Parsing/OptionResult.cs rename to src/System.CommandLine/Parsing/CliOptionResultInternal.cs index e01ca8107d..3c235d4195 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/CliOptionResultInternal.cs @@ -10,23 +10,23 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing an . /// - internal sealed class OptionResult : SymbolResult + internal sealed class CliOptionResultInternal : CliSymbolResultInternal { private ArgumentConversionResult? _argumentConversionResult; - internal OptionResult( + internal CliOptionResultInternal( CliOption option, SymbolResultTree symbolResultTree, CliToken? token = null, - CommandResult? parent = null) : + CliCommandResultInternal? parent = null) : base(symbolResultTree, parent) { Option = option ?? throw new ArgumentNullException(nameof(option)); IdentifierToken = token; } - private ValueResult? _valueResult; - public ValueResult ValueResult + private CliValueResult? _valueResult; + public CliValueResult ValueResult { get { @@ -44,7 +44,7 @@ public ValueResult ValueResult }; var locations = Tokens.Select(token => token.Location).ToArray(); //TODO: Remove this wrapper later - _valueResult = new ValueResult(Option, conversionValue, locations, ArgumentResult.GetValueResultOutcome(ArgumentConversionResult?.Result), conversionResult.ErrorMessage); + _valueResult = new CliValueResult(Option, conversionValue, locations, CliArgumentResultInternal.GetValueResultOutcome(ArgumentConversionResult?.Result), conversionResult.ErrorMessage); } return _valueResult; } @@ -76,7 +76,7 @@ public ValueResult ValueResult public int IdentifierTokenCount { get; internal set; } */ /// - public override string ToString() => $"{nameof(OptionResult)}: {IdentifierToken?.Value ?? Option.Name} {string.Join(" ", Tokens.Select(t => t.Value))}"; + public override string ToString() => $"{nameof(CliOptionResultInternal)}: {IdentifierToken?.Value ?? Option.Name} {string.Join(" ", Tokens.Select(t => t.Value))}"; /// /// Gets the parsed value or the default value for . diff --git a/src/System.CommandLine/Parsing/CliSymbolResult.cs b/src/System.CommandLine/Parsing/CliSymbolResult.cs new file mode 100644 index 0000000000..74284738c1 --- /dev/null +++ b/src/System.CommandLine/Parsing/CliSymbolResult.cs @@ -0,0 +1,9 @@ +// 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 +{ + public class CliSymbolResult + { + } +} \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/CliSymbolResultInternal.cs similarity index 89% rename from src/System.CommandLine/Parsing/SymbolResult.cs rename to src/System.CommandLine/Parsing/CliSymbolResultInternal.cs index e1b49b637d..7c0b6c5fff 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/CliSymbolResultInternal.cs @@ -8,13 +8,13 @@ namespace System.CommandLine.Parsing /// /// A result produced during parsing for a specific symbol. /// - internal abstract class SymbolResult + internal abstract class CliSymbolResultInternal { // TODO: make this a property and protected if possible internal readonly SymbolResultTree SymbolResultTree; private protected List? _tokens; - private protected SymbolResult(SymbolResultTree symbolResultTree, SymbolResult? parent) + private protected CliSymbolResultInternal(SymbolResultTree symbolResultTree, CliSymbolResultInternal? parent) { SymbolResultTree = symbolResultTree; Parent = parent; @@ -38,7 +38,7 @@ public IEnumerable Errors for (var i = 0; i < parseErrors.Count; i++) { var parseError = parseErrors[i]; - if (parseError.SymbolResult == this) + if (parseError.CliSymbolResultInternal == this) { yield return parseError; } @@ -49,7 +49,7 @@ public IEnumerable Errors /// /// The parent symbol result in the parse tree. /// - public SymbolResult? Parent { get; } + public CliSymbolResultInternal? Parent { get; } // TODO: make internal because exposes tokens /// @@ -70,7 +70,7 @@ public IEnumerable Errors /// /// The argument for which to find a result. /// An argument result if the argument was matched by the parser or has a default value; otherwise, null. - internal ArgumentResult? GetResult(CliArgument argument) => SymbolResultTree.GetResult(argument); + internal CliArgumentResultInternal? GetResult(CliArgument argument) => SymbolResultTree.GetResult(argument); /* Not used /// @@ -78,7 +78,7 @@ public IEnumerable Errors /// /// The command for which to find a result. /// An command result if the command was matched by the parser; otherwise, null. - internal CommandResult? GetResult(CliCommand command) => SymbolResultTree.GetResult(command); + internal CliCommandResultInternal? GetResult(CliCommand command) => SymbolResultTree.GetResult(command); */ /// @@ -86,7 +86,7 @@ public IEnumerable Errors /// /// The option for which to find a result. /// An option result if the option was matched by the parser or has a default value; otherwise, null. - internal OptionResult? GetResult(CliOption option) => SymbolResultTree.GetResult(option); + internal CliOptionResultInternal? GetResult(CliOption option) => SymbolResultTree.GetResult(option); // TODO: directives /* @@ -103,7 +103,7 @@ public IEnumerable Errors /// /// The name of the symbol for which to find a result. /// An argument result if the argument was matched by the parser or has a default value; otherwise, null. - public SymbolResult? GetResult(string name) => + public CliSymbolResultInternal? GetResult(string name) => SymbolResultTree.GetResult(name); /// diff --git a/src/System.CommandLine/Parsing/ValueResult.cs b/src/System.CommandLine/Parsing/CliValueResult.cs similarity index 95% rename from src/System.CommandLine/Parsing/ValueResult.cs rename to src/System.CommandLine/Parsing/CliValueResult.cs index 04cc3114b9..96ab4c6a45 100644 --- a/src/System.CommandLine/Parsing/ValueResult.cs +++ b/src/System.CommandLine/Parsing/CliValueResult.cs @@ -8,9 +8,9 @@ namespace System.CommandLine.Parsing; /// /// The publicly facing class for argument and option data. /// -public class ValueResult +public class CliValueResult : CliSymbolResult { - private ValueResult( + private CliValueResult( CliSymbol valueSymbol, object? value, IEnumerable locations, @@ -34,7 +34,7 @@ private ValueResult( /// The locations list. /// True if parsing and converting the value was successful. /// The CliError if parsing or converting failed, otherwise null. - internal ValueResult( + internal CliValueResult( CliArgument argument, object? value, IEnumerable locations, @@ -52,7 +52,7 @@ internal ValueResult( /// The locations list. /// True if parsing and converting the value was successful. /// The CliError if parsing or converting failed, otherwise null. - internal ValueResult( + internal CliValueResult( CliOption option, object? value, IEnumerable locations, @@ -124,7 +124,7 @@ public IEnumerable TextForCommandReconstruction() /// The text the user entered that resulted in this ValueResult. /// public override string ToString() - => $"{nameof(ArgumentResult)} {ValueSymbol.Name}: {string.Join(" ", TextForDisplay())}"; + => $"{nameof(CliArgumentResultInternal)} {ValueSymbol.Name}: {string.Join(" ", TextForDisplay())}"; // TODO: This might not be the right place for this, (Some completion stuff was stripped out. This was a private method in ArgumentConversionResult) diff --git a/src/System.CommandLine/Parsing/DirectiveResult.cs b/src/System.CommandLine/Parsing/DirectiveResult.cs index a1d08544a7..3479071239 100644 --- a/src/System.CommandLine/Parsing/DirectiveResult.cs +++ b/src/System.CommandLine/Parsing/DirectiveResult.cs @@ -5,7 +5,7 @@ namespace System.CommandLine.Parsing /// /// A result produced when parsing an . /// - public sealed class DirectiveResult : SymbolResult + public sealed class DirectiveResult : CliSymbolResultInternal { private List? _values; diff --git a/src/System.CommandLine/Parsing/ParseDiagramAction.cs b/src/System.CommandLine/Parsing/ParseDiagramAction.cs index 474fcfd3af..d8a7b30296 100644 --- a/src/System.CommandLine/Parsing/ParseDiagramAction.cs +++ b/src/System.CommandLine/Parsing/ParseDiagramAction.cs @@ -53,10 +53,10 @@ internal static StringBuilder Diagram(ParseResult parseResult) private static void Diagram( StringBuilder builder, - SymbolResult symbolResult, + CliSymbolResultInternal symbolResult, ParseResult parseResult) { - if (parseResult.Errors.Any(e => e.SymbolResult == symbolResult)) + if (parseResult.Errors.Any(e => e.SymbolResultInternal == symbolResult)) { builder.Append('!'); } @@ -143,10 +143,10 @@ private static void Diagram( } else { - builder.Append(((CommandResult)symbolResult).IdentifierToken.Value); + builder.Append(((CliCommandResultInternal)symbolResult).IdentifierToken.Value); } - foreach (SymbolResult child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) + foreach (CliSymbolResultInternal child in symbolResult.SymbolResultTree.GetChildren(symbolResult)) { if (child is ArgumentResult arg && (arg.Argument.ValueType == typeof(bool) || diff --git a/src/System.CommandLine/Parsing/ParseError.cs b/src/System.CommandLine/Parsing/ParseError.cs index 812a2b209c..9be6314ef3 100644 --- a/src/System.CommandLine/Parsing/ParseError.cs +++ b/src/System.CommandLine/Parsing/ParseError.cs @@ -9,10 +9,10 @@ namespace System.CommandLine.Parsing public sealed class ParseError { // TODO: add position - // TODO: reevaluate whether we should be exposing a SymbolResult here + // TODO: reevaluate whether we should be exposing a CliSymbolResultInternal here internal ParseError( string message, - SymbolResult? symbolResult = null) + CliSymbolResultInternal? symbolResult = null) { if (string.IsNullOrWhiteSpace(message)) { @@ -21,7 +21,7 @@ internal ParseError( Message = message; /* - SymbolResult = symbolResult; + CliSymbolResultInternal = symbolResult; */ } @@ -45,7 +45,7 @@ public ParseError( /// /// The symbol result detailing the symbol that failed to parse and the tokens involved. /// - public SymbolResult? SymbolResult { get; } + public CliSymbolResultInternal? CliSymbolResultInternal { get; } */ /// diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 63f5b03375..3c941ecf97 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -11,10 +11,10 @@ internal sealed class ParseOperation private readonly CliConfiguration _configuration; private readonly string? _rawInput; private readonly SymbolResultTree _symbolResultTree; - private readonly CommandResult _rootCommandResult; + private readonly CliCommandResultInternal _rootCommandResult; private int _index; - private CommandResult _innermostCommandResult; + private CliCommandResultInternal _innermostCommandResult; /* private bool _isHelpRequested; private bool _isTerminatingDirectiveSpecified; @@ -36,7 +36,7 @@ public ParseOperation( _rawInput = rawInput; _symbolResultTree = new(rootCommand, tokenizationErrors); - _innermostCommandResult = _rootCommandResult = new CommandResult( + _innermostCommandResult = _rootCommandResult = new CliCommandResultInternal( rootCommand, CurrentToken, _symbolResultTree); @@ -105,7 +105,7 @@ private void ParseSubcommand() { CliCommand command = (CliCommand)CurrentToken.Symbol!; - _innermostCommandResult = new CommandResult( + _innermostCommandResult = new CliCommandResultInternal( command, CurrentToken, _symbolResultTree, @@ -162,10 +162,10 @@ private void ParseCommandArguments(ref int currentArgumentCount, ref int current } if (!(_symbolResultTree.TryGetValue(argument, out var symbolResult) - && symbolResult is ArgumentResult argumentResult)) + && symbolResult is CliArgumentResultInternal argumentResult)) { argumentResult = - new ArgumentResult( + new CliArgumentResultInternal( argument, _symbolResultTree, _innermostCommandResult); @@ -200,9 +200,9 @@ private void ParseCommandArguments(ref int currentArgumentCount, ref int current private void ParseOption() { CliOption option = (CliOption)CurrentToken.Symbol!; - OptionResult optionResult; + CliOptionResultInternal optionResult; - if (!_symbolResultTree.TryGetValue(option, out SymbolResult? symbolResult)) + if (!_symbolResultTree.TryGetValue(option, out CliSymbolResultInternal? symbolResult)) { // TODO: invocation, directives, help /* @@ -227,7 +227,7 @@ private void ParseOption() } } */ - optionResult = new OptionResult( + optionResult = new CliOptionResultInternal( option, _symbolResultTree, CurrentToken, @@ -237,7 +237,7 @@ private void ParseOption() } else { - optionResult = (OptionResult)symbolResult; + optionResult = (CliOptionResultInternal)symbolResult; } // TODO: IdentifierTokenCount @@ -248,7 +248,7 @@ private void ParseOption() ParseOptionArguments(optionResult); } - private void ParseOptionArguments(OptionResult optionResult) + private void ParseOptionArguments(CliOptionResultInternal optionResult) { var argument = optionResult.Option.Argument; @@ -275,10 +275,10 @@ private void ParseOptionArguments(OptionResult optionResult) break; } - if (!(_symbolResultTree.TryGetValue(argument, out SymbolResult? symbolResult) - && symbolResult is ArgumentResult argumentResult)) + if (!(_symbolResultTree.TryGetValue(argument, out CliSymbolResultInternal? symbolResult) + && symbolResult is CliArgumentResultInternal argumentResult)) { - argumentResult = new ArgumentResult( + argumentResult = new CliArgumentResultInternal( argument, _symbolResultTree, optionResult); @@ -305,7 +305,7 @@ private void ParseOptionArguments(OptionResult optionResult) { if (!_symbolResultTree.ContainsKey(argument)) { - var argumentResult = new ArgumentResult(argument, _symbolResultTree, optionResult); + var argumentResult = new CliArgumentResultInternal(argument, _symbolResultTree, optionResult); _symbolResultTree.Add(argument, argumentResult); } } @@ -397,12 +397,12 @@ private void Validate() // for other commands only a subset of options is checked. _innermostCommandResult.Validate(completeValidation: true); - CommandResult? currentResult = _innermostCommandResult.Parent as CommandResult; + CliCommandResultInternal? currentResult = _innermostCommandResult.Parent as CliCommandResultInternal; while (currentResult is not null) { currentResult.Validate(completeValidation: false); - currentResult = currentResult.Parent as CommandResult; + currentResult = currentResult.Parent as CliCommandResultInternal; } } */ diff --git a/src/System.CommandLine/Parsing/SymbolLookupByName.cs b/src/System.CommandLine/Parsing/SymbolLookupByName.cs index eab4c8ac99..414157c10d 100644 --- a/src/System.CommandLine/Parsing/SymbolLookupByName.cs +++ b/src/System.CommandLine/Parsing/SymbolLookupByName.cs @@ -43,7 +43,7 @@ private List BuildCache(ParseResult parseResult) return cache; } cache = []; - var commandResult = parseResult.CommandResult; + var commandResult = parseResult.CommandResultInternal; while (commandResult is not null) { var command = commandResult.Command; @@ -57,7 +57,7 @@ private List BuildCache(ParseResult parseResult) AddSymbolsToCache(commandCache, command.Options, command); AddSymbolsToCache(commandCache, command.Arguments, command); AddSymbolsToCache(commandCache, command.Subcommands, command); - commandResult = (CommandResult?)commandResult.Parent; + commandResult = (CliCommandResultInternal?)commandResult.Parent; } return cache; @@ -95,7 +95,7 @@ private bool TryGetSymbolAndParentInternal(string name, bool skipAncestors, bool valuesOnly) { - startCommand ??= cache.First().Command; // The construction of the dictionary makes this the parseResult.CommandResult - current command + startCommand ??= cache.First().Command; // The construction of the dictionary makes this the parseResult.CliCommandResultInternal - current command var commandCaches = GetCommandCachesToUse(startCommand); if (commandCaches is null || !commandCaches.Any()) { diff --git a/src/System.CommandLine/Parsing/SymbolResultExtensions.cs b/src/System.CommandLine/Parsing/SymbolResultExtensions.cs index c81459e8c6..ce69867f8c 100644 --- a/src/System.CommandLine/Parsing/SymbolResultExtensions.cs +++ b/src/System.CommandLine/Parsing/SymbolResultExtensions.cs @@ -5,9 +5,9 @@ namespace System.CommandLine.Parsing { - internal static class SymbolResultExtensions + internal static class SymbolResultInternalExtensions { - internal static IEnumerable AllSymbolResults(this CommandResult commandResult) + internal static IEnumerable AllSymbolResults(this CliCommandResultInternal commandResult) { yield return commandResult; diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 74f2501a4d..651890925a 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -6,7 +6,7 @@ namespace System.CommandLine.Parsing { - internal sealed class SymbolResultTree : Dictionary + internal sealed class SymbolResultTree : Dictionary { private readonly CliCommand _rootCommand; internal List? Errors; @@ -37,22 +37,22 @@ internal SymbolResultTree( internal int ErrorCount => Errors?.Count ?? 0; - internal ArgumentResult? GetResult(CliArgument argument) - => TryGetValue(argument, out SymbolResult? result) ? (ArgumentResult)result : default; + internal CliArgumentResultInternal? GetResult(CliArgument argument) + => TryGetValue(argument, out CliSymbolResultInternal? result) ? (CliArgumentResultInternal)result : default; - internal CommandResult? GetResult(CliCommand command) - => TryGetValue(command, out var result) ? (CommandResult)result : default; + internal CliCommandResultInternal? GetResult(CliCommand command) + => TryGetValue(command, out var result) ? (CliCommandResultInternal)result : default; - internal OptionResult? GetResult(CliOption option) - => TryGetValue(option, out SymbolResult? result) ? (OptionResult)result : default; + internal CliOptionResultInternal? GetResult(CliOption option) + => TryGetValue(option, out CliSymbolResultInternal? result) ? (CliOptionResultInternal)result : default; // TODO: Determine how this is used. It appears to be O^n in the size of the tree and so if it is called multiple times, we should reconsider to avoid O^(N*M) - internal IEnumerable GetChildren(SymbolResult parent) + internal IEnumerable GetChildren(CliSymbolResultInternal parent) { // Argument can't have children - if (parent is not ArgumentResult) + if (parent is not CliArgumentResultInternal) { - foreach (KeyValuePair pair in this) + foreach (KeyValuePair pair in this) { if (ReferenceEquals(parent, pair.Value.Parent)) { @@ -62,18 +62,18 @@ internal IEnumerable GetChildren(SymbolResult parent) } } - internal IReadOnlyDictionary BuildValueResultDictionary() + internal IReadOnlyDictionary BuildValueResultDictionary() { - var dict = new Dictionary(); - foreach (KeyValuePair pair in this) + var dict = new Dictionary(); + foreach (KeyValuePair pair in this) { var result = pair.Value; - if (result is OptionResult optionResult) + if (result is CliOptionResultInternal optionResult) { dict.Add(pair.Key, optionResult.ValueResult); continue; } - if (result is ArgumentResult argumentResult) + if (result is CliArgumentResultInternal argumentResult) { dict.Add(pair.Key, argumentResult.ValueResult); continue; @@ -85,7 +85,7 @@ internal IReadOnlyDictionary BuildValueResultDictionary( internal void AddError(ParseError parseError) => (Errors ??= new()).Add(parseError); internal void InsertFirstError(ParseError parseError) => (Errors ??= new()).Insert(0, parseError); - internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, CommandResult rootCommandResult) + internal void AddUnmatchedToken(CliToken token, CliCommandResultInternal commandResult, CliCommandResultInternal rootCommandResult) { /* // TODO: unmatched tokens diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 85e34a8975..56edce283d 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -27,8 +27,8 @@ - - + + @@ -50,19 +50,19 @@ - + - + - + - + - + From 1059fd4089eb0ae450a5b4138cc73113cca7dfad Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Tue, 13 Aug 2024 17:05:38 -0400 Subject: [PATCH 114/150] Updated CliSymbolResult and moved Locations there --- .../Parsing/CliCommandResult.cs | 7 ++++- .../Parsing/CliSymbolResult.cs | 26 +++++++++++++++---- .../Parsing/CliValueResult.cs | 10 +------ 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/System.CommandLine/Parsing/CliCommandResult.cs b/src/System.CommandLine/Parsing/CliCommandResult.cs index 3617df4a3e..513aa1b4a9 100644 --- a/src/System.CommandLine/Parsing/CliCommandResult.cs +++ b/src/System.CommandLine/Parsing/CliCommandResult.cs @@ -17,8 +17,13 @@ public class CliCommandResult : CliSymbolResult /// Creates a CommandValueResult instance /// /// The CliCommand that the result is for. + /// /// The parent command in the case of a CLI hierarchy, or null if there is no parent. - internal CliCommandResult(CliCommand command, CliCommandResult? parent = null) + internal CliCommandResult( + CliCommand command, + IEnumerable locations, + CliCommandResult? parent = null) + : base(locations) { Command = command; Parent = parent; diff --git a/src/System.CommandLine/Parsing/CliSymbolResult.cs b/src/System.CommandLine/Parsing/CliSymbolResult.cs index 74284738c1..35353a7987 100644 --- a/src/System.CommandLine/Parsing/CliSymbolResult.cs +++ b/src/System.CommandLine/Parsing/CliSymbolResult.cs @@ -1,9 +1,25 @@ // 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 +using System.Collections.Generic; + +namespace System.CommandLine.Parsing; + +/// +/// Base class for CliValueResult and CliCommandResult. +/// +/// +/// Common values such as `TextForDisplay` are expected +/// +public abstract class CliSymbolResult(IEnumerable locations) { - public class CliSymbolResult - { - } -} \ No newline at end of file + /// + /// Gets the locations at which the tokens that made up the value appeared. + /// + /// + /// This needs to be a collection for CliValueType because collection types have + /// multiple tokens and they will not be simple offsets when response files are used. + /// + public IEnumerable Locations { get; } = locations; + +} diff --git a/src/System.CommandLine/Parsing/CliValueResult.cs b/src/System.CommandLine/Parsing/CliValueResult.cs index 96ab4c6a45..a894154444 100644 --- a/src/System.CommandLine/Parsing/CliValueResult.cs +++ b/src/System.CommandLine/Parsing/CliValueResult.cs @@ -17,10 +17,10 @@ private CliValueResult( ValueResultOutcome outcome, // TODO: Error should be an Enumerable and perhaps should not be here at all, only on ParseResult string? error = null) + : base(locations) { ValueSymbol = valueSymbol; Value = value; - Locations = locations; Outcome = outcome; // TODO: Probably a collection of errors here Error = error; @@ -79,14 +79,6 @@ internal CliValueResult( ? default : (T?)Value; - /// - /// Gets the locations at which the tokens that made up the value appeared. - /// - /// - /// This needs to be a collection because collection types have multiple tokens and they will not be simple offsets when response files are used. - /// - public IEnumerable Locations { get; } - /// /// True when parsing and converting the value was successful /// From 92066402d806914b3cd2683edb0f83d7162368b8 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Fri, 16 Aug 2024 16:35:42 -0400 Subject: [PATCH 115/150] Fixes after rebase --- src/System.CommandLine.Tests/ParserTests.cs | 4 ++-- src/System.CommandLine/ParseResult.cs | 6 +++--- src/System.CommandLine/Parsing/CliArgumentResultInternal.cs | 2 +- src/System.CommandLine/Parsing/CliCommandResult.cs | 2 +- src/System.CommandLine/Parsing/CliCommandResultInternal.cs | 5 +++-- src/System.CommandLine/System.CommandLine.csproj | 1 + 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index b832c80503..f6af64cbc4 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -141,7 +141,7 @@ public void Option_short_forms_can_be_bundled() result.CommandResult .ValueResults - .Select(o => ((OptionResult)o).Option.Name) + .Select(o => ((CliValueResult)o).ValueSymbol.Name) .Should() .BeEquivalentTo("-x", "-y", "-z"); } @@ -1714,7 +1714,7 @@ public void CommandResult_contains_argument_ValueResults() commandResult.ValueResults.Should().HaveCount(2); var result1 = commandResult.ValueResults[0]; result1.GetValue().Should().Be("Kirk"); - var result2 = commandValueResult.ValueResults[1]; + var result2 = commandResult.ValueResults[1]; result2.GetValue().Should().Be("Spock"); } diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 4471c3bfc3..5a245913e4 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -32,7 +32,7 @@ internal ParseResult( // TODO: Remove RootCommandResult - it is the root of the CommandValueResult ancestors (fix that) CliCommandResultInternal rootCommandResult, // TODO: Replace with CommandValueResult and remove CommandResult - CliCommandResultInternal commandResult, + CliCommandResultInternal commandResultInternal, IReadOnlyDictionary valueResultDictionary, /* List tokens, @@ -52,8 +52,8 @@ internal ParseResult( Configuration = configuration; _rootCommandResult = rootCommandResult; // TODO: Why do we need this? - CommandResultInternal = commandResult; - CommandResult = commandResult.CommandValueResult; + CommandResultInternal = commandResultInternal; + CommandResult = commandResultInternal.CommandResult; this.valueResultDictionary = valueResultDictionary; // TODO: invocation /* diff --git a/src/System.CommandLine/Parsing/CliArgumentResultInternal.cs b/src/System.CommandLine/Parsing/CliArgumentResultInternal.cs index db525fe4ee..a6479af553 100644 --- a/src/System.CommandLine/Parsing/CliArgumentResultInternal.cs +++ b/src/System.CommandLine/Parsing/CliArgumentResultInternal.cs @@ -33,7 +33,7 @@ public CliValueResult ValueResult // TODO: Make sure errors are added var conversionValue = GetArgumentConversionResult().Value; var locations = Tokens.Select(token => token.Location).ToArray(); - _valueResult = new ValueResult(Argument, conversionValue, locations, ArgumentResult.GetValueResultOutcome(GetArgumentConversionResult()?.Result)); // null is temporary here + _valueResult = new CliValueResult(Argument, conversionValue, locations, CliArgumentResultInternal.GetValueResultOutcome(GetArgumentConversionResult()?.Result)); // null is temporary here } return _valueResult; } diff --git a/src/System.CommandLine/Parsing/CliCommandResult.cs b/src/System.CommandLine/Parsing/CliCommandResult.cs index 513aa1b4a9..2d1fa10848 100644 --- a/src/System.CommandLine/Parsing/CliCommandResult.cs +++ b/src/System.CommandLine/Parsing/CliCommandResult.cs @@ -32,7 +32,7 @@ internal CliCommandResult( /// /// The ValueResult instances for user entered data. This is a sparse list. /// - public IEnumerable ValueResults { get; } = []; + public IReadOnlyList ValueResults { get; internal set; } = []; /// /// The CliCommand that the result is for. diff --git a/src/System.CommandLine/Parsing/CliCommandResultInternal.cs b/src/System.CommandLine/Parsing/CliCommandResultInternal.cs index 54172c3832..3d6318de0f 100644 --- a/src/System.CommandLine/Parsing/CliCommandResultInternal.cs +++ b/src/System.CommandLine/Parsing/CliCommandResultInternal.cs @@ -47,9 +47,10 @@ public CliCommandResult CommandResult if (commandResult is null) { var parent = Parent is CliCommandResultInternal commandResultInternal - ? commandResultInternal.CommandValueResult + ? commandResultInternal.CommandResult : null; - commandResult = new CliCommandResult(Command, parent); + // TODO: Resolve the NRT warning for locations + commandResult = new CliCommandResult(Command, parent?.Locations, parent); } // Reset unless we put tests in place to ensure it is not called in error handling before SymbolTree processing is complete commandResult.ValueResults = Children.Select(GetValueResult).OfType().ToList(); diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 56edce283d..16229d529e 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -27,6 +27,7 @@ + From e1bf3f5d0897b21a4cd7c33a1e8a541de5714cc7 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Fri, 16 Aug 2024 17:05:40 -0400 Subject: [PATCH 116/150] Fixed NRT error --- src/System.CommandLine/Parsing/CliCommandResultInternal.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/System.CommandLine/Parsing/CliCommandResultInternal.cs b/src/System.CommandLine/Parsing/CliCommandResultInternal.cs index 3d6318de0f..764ecf824b 100644 --- a/src/System.CommandLine/Parsing/CliCommandResultInternal.cs +++ b/src/System.CommandLine/Parsing/CliCommandResultInternal.cs @@ -47,10 +47,9 @@ public CliCommandResult CommandResult if (commandResult is null) { var parent = Parent is CliCommandResultInternal commandResultInternal - ? commandResultInternal.CommandResult + ? commandResultInternal.CommandResult : null; - // TODO: Resolve the NRT warning for locations - commandResult = new CliCommandResult(Command, parent?.Locations, parent); + commandResult = new CliCommandResult(Command, Tokens.Select(t => t.Location), parent); } // Reset unless we put tests in place to ensure it is not called in error handling before SymbolTree processing is complete commandResult.ValueResults = Children.Select(GetValueResult).OfType().ToList(); From 59201ca7e5e71b0d47baf1c4a1c7d3495eef84d0 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 10 Aug 2024 13:51:38 -0400 Subject: [PATCH 117/150] Changed ValueSubsystem to ValueProvider ValueProvider does not execute or have any characteristics of a subsystem This commit included the other PRs from this morning to get it building and testing. Messy. --- .../ValueSubsystemTests.cs | 34 +-- .../Directives/DiagramSubsystem.cs | 136 ++++----- src/System.CommandLine.Subsystems/Pipeline.cs | 16 +- .../PipelineResult.cs | 3 + .../Subsystems/PipelinePhase.cs | 2 +- .../ValueAnnotationExtensions.cs | 8 +- .../{ValueSubsystem.cs => ValueProvider.cs} | 33 +-- src/System.CommandLine.Tests/ParserTests.cs | 257 +++++++++--------- src/System.CommandLine/CliArgument{T}.cs | 3 +- src/System.CommandLine/CliOption.cs | 1 - src/System.CommandLine/ParseResult.cs | 21 +- .../Parsing/CliValueResult.cs | 23 +- 12 files changed, 273 insertions(+), 264 deletions(-) rename src/System.CommandLine.Subsystems/{ValueSubsystem.cs => ValueProvider.cs} (68%) diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index 797fb748fc..4284562994 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -10,25 +10,25 @@ namespace System.CommandLine.Subsystems.Tests; public class ValueSubsystemTests { - [Fact] - public void ValueSubsystem_is_activated_by_default() - { - CliRootCommand rootCommand = [ - new CliCommand("x") - { - new CliOption("--opt1") - }]; - var configuration = new CliConfiguration(rootCommand); - var subsystem = new ValueSubsystem(); - var input = "x --opt1 Kirk"; - var args = CliParser.SplitCommandLine(input).ToList(); + //[Fact] + //public void ValueSubsystem_is_activated_by_default() + //{ + // CliRootCommand rootCommand = [ + // new CliCommand("x") + // { + // new CliOption("--opt1") + // }]; + // var configuration = new CliConfiguration(rootCommand); + // var subsystem = new ValueProvider(); + // var input = "x --opt1 Kirk"; + // var args = CliParser.SplitCommandLine(input).ToList(); - Subsystem.Initialize(subsystem, configuration, args); - var parseResult = CliParser.Parse(rootCommand, args[0], configuration); - var isActive = Subsystem.GetIsActivated(subsystem, parseResult); + // Subsystem.Initialize(subsystem, configuration, args); + // var parseResult = CliParser.Parse(rootCommand, args[0], configuration); + // var isActive = Subsystem.GetIsActivated(subsystem, parseResult); - isActive.Should().BeTrue(); - } + // isActive.Should().BeTrue(); + //} /* Hold these tests until we determine if ValueSubsystem is replaceable [Fact] public void ValueSubsystem_returns_values_that_are_entered() diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index f05627aa1f..3984632bc0 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -76,78 +76,78 @@ private static void Diagram( break; */ - // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives - /* - case ArgumentResult argumentResult: + // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives + /* + case ArgumentResult argumentResult: + { + var includeArgumentName = + argumentResult.Argument.FirstParent!.Symbol is CliCommand { HasArguments: true, Arguments.Count: > 1 }; + + if (includeArgumentName) { - var includeArgumentName = - argumentResult.Argument.FirstParent!.Symbol is CliCommand { HasArguments: true, Arguments.Count: > 1 }; + builder.Append("[ "); + builder.Append(argumentResult.Argument.Name); + builder.Append(' '); + } - if (includeArgumentName) + if (argumentResult.Argument.Arity.MaximumNumberOfValues > 0) + { + ArgumentConversionResult conversionResult = argumentResult.GetArgumentConversionResult(); + switch (conversionResult.Result) { - builder.Append("[ "); - builder.Append(argumentResult.Argument.Name); - builder.Append(' '); + case ArgumentConversionResultType.NoArgument: + break; + case ArgumentConversionResultType.Successful: + switch (conversionResult.Value) + { + case string s: + builder.Append($"<{s}>"); + break; + + case IEnumerable items: + builder.Append('<'); + builder.Append( + string.Join("> <", + items.Cast().ToArray())); + builder.Append('>'); + break; + + default: + builder.Append('<'); + builder.Append(conversionResult.Value); + builder.Append('>'); + break; + } + + break; + + default: // failures + builder.Append('<'); + builder.Append(string.Join("> <", symbolResult.Tokens.Select(t => t.Value))); + builder.Append('>'); + + break; } + } - if (argumentResult.Argument.Arity.MaximumNumberOfValues > 0) - { - ArgumentConversionResult conversionResult = argumentResult.GetArgumentConversionResult(); - switch (conversionResult.Result) - { - case ArgumentConversionResultType.NoArgument: - break; - case ArgumentConversionResultType.Successful: - switch (conversionResult.Value) - { - case string s: - builder.Append($"<{s}>"); - break; - - case IEnumerable items: - builder.Append('<'); - builder.Append( - string.Join("> <", - items.Cast().ToArray())); - builder.Append('>'); - break; - - default: - builder.Append('<'); - builder.Append(conversionResult.Value); - builder.Append('>'); - break; - } - - break; - - default: // failures - builder.Append('<'); - builder.Append(string.Join("> <", symbolResult.Tokens.Select(t => t.Value))); - builder.Append('>'); - - break; - } - } + if (includeArgumentName) + { + builder.Append(" ]"); + } - if (includeArgumentName) - { - builder.Append(" ]"); - } + break; + } - break; - } + default: + { + OptionResult? optionResult = symbolResult as OptionResult; - default: + if (optionResult is { Implicit: true }) { - OptionResult? optionResult = symbolResult as OptionResult; + builder.Append('*'); + } - if (optionResult is { Implicit: true }) - { - builder.Append('*'); - } - - builder.Append("[ "); + builder.Append("[ "); if (optionResult is not null) { @@ -167,16 +167,16 @@ private static void Diagram( continue; } - builder.Append(' '); - - Diagram(builder, child, parseResult); - } + builder.Append(' '); - builder.Append(" ]"); - break; + Diagram(builder, child, parseResult); } + + builder.Append(" ]"); + break; } } } +} */ } diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index a223113547..3a568e4776 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -28,7 +28,7 @@ public partial class Pipeline /// A help subsystem to replace the standard one. To add a subsystem, use /// A new pipeline. /// - /// Currently, the standard , , and cannot be replaced. is disabled by default. + /// The ValueProvider, ResponseSubystem, InvocationSubsystem, and ValidationSubsystem cannot be replaced. /// public static Pipeline Create(HelpSubsystem? help = null, VersionSubsystem? version = null, @@ -56,7 +56,7 @@ public static Pipeline CreateEmpty() private Pipeline() { - Value = new ValueSubsystem(); + //Value = new ValueProvider(); Response = new ResponseSubsystem(); Invocation = new InvocationSubsystem(); Validation = new ValidationSubsystem(); @@ -172,20 +172,20 @@ public ErrorReportingSubsystem? ErrorReporting // TODO: Consider whether replacing the validation subsystem is valuable /// - /// Sets or gets the validation subsystem + /// Gets the validation subsystem /// public ValidationSubsystem? Validation { get; } // TODO: Consider whether replacing the invocation subsystem is valuable /// - /// Sets or gets the invocation subsystem + /// Gets the invocation subsystem /// public InvocationSubsystem? Invocation { get; } - /// - /// Gets the value subsystem which manages entered and default values. - /// - public ValueSubsystem Value { get; } + ///// + ///// Gets the value subsystem which manages entered and default values. + ///// + //public ValueProvider Value { get; } /// /// Gets the response file subsystem diff --git a/src/System.CommandLine.Subsystems/PipelineResult.cs b/src/System.CommandLine.Subsystems/PipelineResult.cs index 8762a68720..a5482794d2 100644 --- a/src/System.CommandLine.Subsystems/PipelineResult.cs +++ b/src/System.CommandLine.Subsystems/PipelineResult.cs @@ -9,7 +9,10 @@ public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? { private readonly List errors = []; public ParseResult? ParseResult { get; } = parseResult; + private ValueProvider valueProvider { get; } = new ValueProvider(parseResult); public string RawInput { get; } = rawInput; + + // TODO: Consider behavior when pipeline is null - this is probably a core user accessing some subsystems public Pipeline Pipeline { get; } = pipeline ?? Pipeline.CreateEmpty(); public ConsoleHack ConsoleHack { get; } = consoleHack ?? new ConsoleHack(); diff --git a/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs b/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs index 220b3217a9..023b7f779a 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/PipelinePhase.cs @@ -41,7 +41,7 @@ private List CreateAfterIfNeeded() return after; } - public IEnumerable GetSubsystems() + public IEnumerable GetSubsystems() => [ .. (before is null ? [] : before), .. (CliSubsystem is null ? new List { } : [CliSubsystem]), diff --git a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs index 137b1f8982..7506e45a5f 100644 --- a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs @@ -39,7 +39,7 @@ public static void SetDefaultValue(this CliOption option, TValue /// The option /// The option's default value annotation if any, otherwise /// - /// This is intended to be called by CLI authors. Subsystems should instead call , + /// This is intended to be called by CLI authors. Subsystems should instead call , /// which calculates the actual default value, based on the default value annotation and default value calculation, /// whether directly stored on the symbol or from the subsystem's . /// @@ -84,7 +84,7 @@ public static void SetDefaultValue(this CliArgument argument, TV /// The argument /// The argument's default value annotation if any, otherwise /// - /// This is intended to be called by CLI authors. Subsystems should instead call , + /// This is intended to be called by CLI authors. Subsystems should instead call , /// which calculates the actual default value, based on the default value annotation and default value calculation, /// whether directly stored on the symbol or from the subsystem's . /// @@ -128,7 +128,7 @@ public static void SetDefaultValueCalculation(this CliOption opt /// The option /// The option's default value calculation if any, otherwise /// - /// This is intended to be called by CLI authors. Subsystems should instead call , + /// This is intended to be called by CLI authors. Subsystems should instead call , /// which calculates the actual default value, based on the default value annotation and default value calculation, /// whether directly stored on the symbol or from the subsystem's . /// @@ -173,7 +173,7 @@ public static void SetDefaultValueCalculation(this CliArgument a /// The argument /// The argument's default value calculation if any, otherwise /// - /// This is intended to be called by CLI authors. Subsystems should instead call , + /// This is intended to be called by CLI authors. Subsystems should instead call , /// which calculates the actual default value, based on the default value annotation and default value calculation, /// whether directly stored on the symbol or from the subsystem's . /// diff --git a/src/System.CommandLine.Subsystems/ValueSubsystem.cs b/src/System.CommandLine.Subsystems/ValueProvider.cs similarity index 68% rename from src/System.CommandLine.Subsystems/ValueSubsystem.cs rename to src/System.CommandLine.Subsystems/ValueProvider.cs index f0c334239b..fd30243db8 100644 --- a/src/System.CommandLine.Subsystems/ValueSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValueProvider.cs @@ -6,36 +6,17 @@ namespace System.CommandLine; -public class ValueSubsystem(IAnnotationProvider? annotationProvider = null) - : CliSubsystem(ValueAnnotations.Prefix, SubsystemKind.Value, annotationProvider) +internal class ValueProvider { private Dictionary cachedValues = []; private ParseResult? parseResult = null; - // It is possible that another subsystems GetIsActivated method will access a value. - // If this is called from a GetIsActivated method of a subsystem in the early termination group, - // it will fail. That is not an expected scenario. - /// - /// - /// Note to inheritors: Call base for all ValueSubsystem methods that you override to ensure correct behavior - /// - protected internal override bool GetIsActivated(ParseResult? parseResult) + public ValueProvider(ParseResult parseResult) { this.parseResult = parseResult; - return true; } - /// - /// - /// Note to inheritors: Call base for all ValueSubsystem methods that you override to ensure correct behavior - /// - protected internal override void Execute(PipelineResult pipelineResult) - { - parseResult ??= pipelineResult.ParseResult; - base.Execute(pipelineResult); - } - - private void SetValue(CliSymbol symbol, object? value) + private void SetValue(CliSymbol symbol, object? value) { cachedValues[symbol] = value; } @@ -53,10 +34,8 @@ private bool TryGetValue(CliSymbol symbol, out T? value) return false; } - public T? GetValue(CliOption option) - => GetValueInternal(option); - public T? GetValue(CliArgument argument) - => GetValueInternal(argument); + public T? GetValue(CliDataSymbol dataSymbol) + => GetValueInternal(dataSymbol); private T? GetValueInternal(CliSymbol? symbol) { @@ -84,7 +63,7 @@ not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out T? exp TValue? UseValue(CliSymbol symbol, TValue? value) { - SetValue(symbol, value); + SetValue(symbol, value); return value; } } diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index f6af64cbc4..b708448ffe 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -146,8 +146,7 @@ public void Option_short_forms_can_be_bundled() .BeEquivalentTo("-x", "-y", "-z"); } - /* - + /* Retain this test, but not sure how to test outcome. UnmatchedTokens removed for now. [Fact] public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off() { @@ -167,9 +166,9 @@ public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off EnablePosixBundling = false }; - var result = rootCommand.Parse("the-command -xyz", configuration); + var result = CliParser.Parse(rootCommand, "the-command -xyz", configuration); - result.UnmatchedTokens + result.CommandResult.UnmatchedTokens .Should() .BeEquivalentTo("-xyz"); } @@ -365,14 +364,13 @@ public void Parser_root_Options_can_be_specified_multiple_times_and_their_argume .BeEquivalentTo("carrot"); } - /* [Fact] public void Options_can_be_specified_multiple_times_and_their_arguments_are_collated() { + var animalsOption = new CliOption("-a", "--animals"); // TODO: tests AcceptOnlyFromAmong, fix // TODO: This test does not appear to use AcceptOnlyFromAmong. Consider if test can just use normal strings - var animalsOption = new CliOption("-a", "--animals"); - animalsOption.AcceptOnlyFromAmong("dog", "cat", "sheep"); + //animalsOption.AcceptOnlyFromAmong("dog", "cat", "sheep"); var vegetablesOption = new CliOption("-v", "--vegetables"); CliCommand command = new CliCommand("the-command") { @@ -380,7 +378,7 @@ public void Options_can_be_specified_multiple_times_and_their_arguments_are_coll vegetablesOption }; - var result = command.Parse("the-command -a cat -v carrot -a dog"); + var result = CliParser.Parse(command, "the-command -a cat -v carrot -a dog"); result.GetResult(animalsOption) .Tokens @@ -394,7 +392,6 @@ public void Options_can_be_specified_multiple_times_and_their_arguments_are_coll .Should() .BeEquivalentTo("carrot"); } - */ [Fact] public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_following_token_is_considered_an_argument_to_the_parent_command() @@ -524,7 +521,7 @@ public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string c } */ - /* + /* These two tests should remain in core, this method testing will not work in core. Figure out another way to validate result. [Fact] public void An_outer_command_with_the_same_name_does_not_capture() { @@ -589,7 +586,7 @@ public void When_nested_commands_all_accept_arguments_then_the_nearest_captures_ .BeEquivalentTo("arg2"); } - /* + /* This test should remain in core, this method testing will not work in core. Figure out another way to validate result. [Fact] public void Nested_commands_with_colliding_names_cannot_both_be_applied() { @@ -737,10 +734,8 @@ public void When_options_with_the_same_name_are_defined_on_parent_and_child_comm .ContainSingle(o => o is CliOptionResultInternal && ((CliOptionResultInternal)o).Option.Name == "-x"); } - /* - + /* Tests unmatched tokens, needs fix [Fact] - // TODO: tests unmatched tokens, needs fix public void Arguments_only_apply_to_the_nearest_command() { var outer = new CliCommand("outer") @@ -858,127 +853,126 @@ public void Absolute_Windows_style_paths_are_lexed_correctly() .OnlyContain(a => a.Value == @"c:\temp\the file.txt\"); } -// TODO: Default values -/* - [Fact] - public void Commands_can_have_default_argument_values() - { - var argument = new CliArgument("the-arg") - { - DefaultValueFactory = (_) => "default" - }; + /* These tests should be split and those using an explicit default value moved to subsystem, and those using the type default should remain in core (?). This might not be meaningful if the type conversion is correct. What value other than the type default could be used. + [Fact] + public void Commands_can_have_default_argument_values() + { + var argument = new CliArgument("the-arg") + { + DefaultValueFactory = (_) => "default" + }; - var command = new CliCommand("command") - { - argument - }; + var command = new CliCommand("command") + { + argument + }; - ParseResult result = CliParser.Parse(command, "command"); + ParseResult result = CliParser.Parse(command, "command"); - GetValue(result, argument) - .Should() - .Be("default"); - } + GetValue(result, argument) + .Should() + .Be("default"); + } - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_the_option_can_still_be_accessed_as_though_it_had_been_applied() - { - var command = new CliCommand("command"); - var option = new CliOption("-o", "--option") - { - DefaultValueFactory = (_) => "the-default" - }; - command.Options.Add(option); + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_the_option_can_still_be_accessed_as_though_it_had_been_applied() + { + var command = new CliCommand("command"); + var option = new CliOption("-o", "--option") + { + DefaultValueFactory = (_) => "the-default" + }; + command.Options.Add(option); - ParseResult result = CliParser.Parse(command, "command"); + ParseResult result = CliParser.Parse(command, "command"); - result.GetResult(option).Should().NotBeNull(); - GetValue(result, option).Should().Be("the-default"); - } + result.GetResult(option).Should().NotBeNull(); + GetValue(result, option).Should().Be("the-default"); + } - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_the_option_result_is_implicit() - { - var option = new CliOption("-o", "--option") - { - DefaultValueFactory = (_) => "the-default" - }; + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_the_option_result_is_implicit() + { + var option = new CliOption("-o", "--option") + { + DefaultValueFactory = (_) => "the-default" + }; - var command = new CliCommand("command") - { - option - }; + var command = new CliCommand("command") + { + option + }; - var result = CliParser.Parse(command, "command"); + var result = CliParser.Parse(command, "command"); - result.GetResult(option) - .Implicit - .Should() - .BeTrue(); - } + result.GetResult(option) + .Implicit + .Should() + .BeTrue(); + } - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_there_are_no_tokens() - { - var option = new CliOption("-o") - { - DefaultValueFactory = (_) => "the-default" - }; + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_there_are_no_tokens() + { + var option = new CliOption("-o") + { + DefaultValueFactory = (_) => "the-default" + }; - var command = new CliCommand("command") - { - option - }; + var command = new CliCommand("command") + { + option + }; - var result = CliParser.Parse(command, "command"); + var result = CliParser.Parse(command, "command"); - result.GetResult(option) - .IdentifierToken - .Should() - .BeEquivalentTo(default(CliToken)); - } + result.GetResult(option) + .IdentifierToken + .Should() + .BeEquivalentTo(default(CliToken)); + } - [Fact] - public void When_an_argument_with_a_default_value_is_not_matched_then_there_are_no_tokens() - { - var argument = new CliArgument("o") - { - DefaultValueFactory = (_) => "the-default" - }; + [Fact] + public void When_an_argument_with_a_default_value_is_not_matched_then_there_are_no_tokens() + { + var argument = new CliArgument("o") + { + DefaultValueFactory = (_) => "the-default" + }; - var command = new CliCommand("command") - { - argument - }; - var result = CliParser.Parse(command, "command"); + var command = new CliCommand("command") + { + argument + }; + var result = CliParser.Parse(command, "command"); - result.GetResult(argument) - .Tokens - .Should() - .BeEmpty(); - } + result.GetResult(argument) + .Tokens + .Should() + .BeEmpty(); + } - [Fact] - public void Command_default_argument_value_does_not_override_parsed_value() - { - var argument = new CliArgument("the-arg") - { - DefaultValueFactory = (_) => new DirectoryInfo(Directory.GetCurrentDirectory()) - }; + [Fact] + public void Command_default_argument_value_does_not_override_parsed_value() + { + var argument = new CliArgument("the-arg") + { + DefaultValueFactory = (_) => new DirectoryInfo(Directory.GetCurrentDirectory()) + }; - var command = new CliCommand("inner") - { - argument - }; + var command = new CliCommand("inner") + { + argument + }; - var result = CliParser.Parse(command, "the-directory"); + var result = CliParser.Parse(command, "the-directory"); - GetValue(result, argument) - .Name - .Should() - .Be("the-directory"); - } -*/ + GetValue(result, argument) + .Name + .Should() + .Be("the-directory"); + } + */ [Fact] public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_tokens() @@ -1003,7 +997,7 @@ public void Unmatched_tokens_that_look_like_options_are_not_split_into_smaller_t .BeEquivalentTo("-p:RandomThing=random"); } - /* + /* Unmatched tokens [Fact] public void The_default_behavior_of_unmatched_tokens_resulting_in_errors_can_be_turned_off() { @@ -1117,10 +1111,9 @@ public void When_an_option_argument_is_enclosed_in_double_quotes_its_value_retai .BeEquivalentTo(new[] { arg2 }); } - // TODO: Tests tokens which is no longer exposed, and should be replaced by tests of location or removed - /* + /* Tests tokens which is no longer exposed, and should be replaced by tests of location or removed [Fact] // https://github.com/dotnet/command-line-api/issues/1445 - public void Trailing_option_delimiters_are_ignored() + public void Trailing_option_delimiters_are_ignored() // (colon after directory) { var rootCommand = new CliRootCommand { @@ -1362,7 +1355,7 @@ public void Boolean_options_with_no_argument_specified_do_not_match_subsequent_a GetValue(result, option).Should().BeTrue(); } - /* + /* Unmatched tokens [Fact] public void When_a_command_line_has_unmatched_tokens_they_are_not_applied_to_subsequent_options() { @@ -1501,7 +1494,7 @@ public void Command_argument_arity_can_be_a_range_with_a_lower_bound_greater_tha new CliToken("5", CliTokenType.Argument, argument, dummyLocation)); } - [Fact(Skip ="Waiting for CliError work")] + [Fact(Skip = "Waiting for CliError work")] public void When_command_arguments_are_fewer_than_minimum_arity_then_an_error_is_returned() { var command = new CliCommand("the-command") @@ -1626,8 +1619,7 @@ public void When_option_arguments_are_greater_than_maximum_arity_then_an_error_i .Contain(LocalizationResources.UnrecognizedCommandOrArgument("4")); } - // TODO: Tests tokens which is no longer exposed, and should be replaced with equivalent test using ParseResult - /* + /* Tokens [Fact] public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_option() { @@ -1642,7 +1634,8 @@ public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_opti "jdbc:sqlserver://10.0.0.2;databaseName=main"); } */ - /* + + /* Completions. Move to subsystem. [Fact] public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() { @@ -1692,7 +1685,7 @@ public void Parsed_value_of_empty_string_arg_is_an_empty_string(string arg1, str GetValue(result, option).Should().BeEmpty(); } - // TODO: Tests below are from Powderhouse. Consider whether this the right location considering how large the file is + // TODO: Tests below are from Powderhouse. Consider whether this the right location considering how large the file is. Consider `Trait("Version", "Powderhouse")]` [Fact] public void CommandResult_contains_argument_ValueResults() { @@ -1713,9 +1706,9 @@ public void CommandResult_contains_argument_ValueResults() var commandResult = parseResult.CommandResult; commandResult.ValueResults.Should().HaveCount(2); var result1 = commandResult.ValueResults[0]; - result1.GetValue().Should().Be("Kirk"); + result1.GetValue().Should().Be("Kirk"); var result2 = commandResult.ValueResults[1]; - result2.GetValue().Should().Be("Spock"); + result2.GetValue().Should().Be("Spock"); } [Fact] @@ -1738,9 +1731,9 @@ public void CommandResult_contains_option_ValueResults() var commandResult = parseResult.CommandResult; commandResult.ValueResults.Should().HaveCount(2); var result1 = commandResult.ValueResults[0]; - result1.GetValue().Should().Be("Kirk"); + result1.GetValue().Should().Be("Kirk"); var result2 = commandResult.ValueResults[1]; - result2.GetValue().Should().Be("Spock"); + result2.GetValue().Should().Be("Spock"); } [Fact] @@ -1914,8 +1907,8 @@ public void ParseResult_contains_argument_ValueResults() var result1 = parseResult.GetValueResult(argument1); var result2 = parseResult.GetValueResult(argument2); - result1.GetValue().Should().Be("Kirk"); - result2.GetValue().Should().Be("Spock"); + result1.GetValue().Should().Be("Kirk"); + result2.GetValue().Should().Be("Spock"); } [Fact] @@ -1938,8 +1931,8 @@ public void ParseResult_contains_option_ValueResults() var result1 = parseResult.GetValueResult(option1); var result2 = parseResult.GetValueResult(option2); - result1.GetValue().Should().Be("Kirk"); - result2.GetValue().Should().Be("Spock"); + result1.GetValue().Should().Be("Kirk"); + result2.GetValue().Should().Be("Spock"); } } } diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index d9ac656f1d..a1949a3b0a 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -20,7 +20,8 @@ public class CliArgument : CliArgument /// /// The name of the argument. It's not used for parsing, only when displaying Help or creating parse errors.> /// - public CliArgument(string name) : base(name) + public CliArgument(string name) + : base(name) { } diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index ae0a9b7ebf..fc9bdabe9b 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -15,7 +15,6 @@ public abstract class CliOption : CliValueSymbol internal AliasSet? _aliases; /* private List>? _validators; - */ private protected CliOption(string name, string[] aliases) diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 5a245913e4..e918f844a4 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -102,10 +102,25 @@ internal ParseResult( /// /// A result indicating the command specified in the command line input. /// - // TODO: Update SymbolLookupByName to use CommandValueResult, then remove - internal CliCommandResultInternal CommandResultInternal { get; } + internal CommandResult CommandResult { get; } + + private CommandValueResult? commandValueResult = null; + public CommandValueResult CommandValueResult + { + get + { + if (commandValueResult is null) + { + commandValueResult = new CommandValueResult(CommandResult); + } + return commandValueResult; + } + } + + public IEnumerable AllValueResults + => valueResultDictionary.Values; + - public CliCommandResult CommandResult { get; } /// /// The configuration used to produce the parse result. diff --git a/src/System.CommandLine/Parsing/CliValueResult.cs b/src/System.CommandLine/Parsing/CliValueResult.cs index a894154444..8286dbc4e6 100644 --- a/src/System.CommandLine/Parsing/CliValueResult.cs +++ b/src/System.CommandLine/Parsing/CliValueResult.cs @@ -11,7 +11,7 @@ namespace System.CommandLine.Parsing; public class CliValueResult : CliSymbolResult { private CliValueResult( - CliSymbol valueSymbol, + CliValueSymbol valueSymbol, object? value, IEnumerable locations, ValueResultOutcome outcome, @@ -65,7 +65,7 @@ internal CliValueResult( /// /// The CliSymbol the value is for. This is always a CliOption or CliArgument. /// - public CliSymbol ValueSymbol { get; } + public CliDataSymbol ValueSymbol { get; } internal object? Value { get; } @@ -79,6 +79,25 @@ internal CliValueResult( ? default : (T?)Value; + /// + /// Returns the value, or the default for the type. + /// + /// The type to return + /// The value, cast to the requested type. + public object? GetValue() + => Value is null + ? default + : Value; + + + /// + /// Gets the locations at which the tokens that made up the value appeared. + /// + /// + /// This needs to be a collection because collection types have multiple tokens and they will not be simple offsets when response files are used. + /// + public IEnumerable Locations { get; } + /// /// True when parsing and converting the value was successful /// From cc1c3a5d92d2d5f66d1f5d2e4515330c37eacc7f Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 17 Aug 2024 11:50:30 -0400 Subject: [PATCH 118/150] WIP --- src/System.CommandLine.Subsystems/ValueProvider.cs | 8 ++++---- src/System.CommandLine.Tests/ParserTests.cs | 12 ++++++------ src/System.CommandLine/ParseResult.cs | 13 ++++++------- src/System.CommandLine/Parsing/CliValueResult.cs | 6 +++--- .../Parsing/SymbolLookupByName.cs | 2 +- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/System.CommandLine.Subsystems/ValueProvider.cs b/src/System.CommandLine.Subsystems/ValueProvider.cs index fd30243db8..6741ff3400 100644 --- a/src/System.CommandLine.Subsystems/ValueProvider.cs +++ b/src/System.CommandLine.Subsystems/ValueProvider.cs @@ -34,8 +34,8 @@ private bool TryGetValue(CliSymbol symbol, out T? value) return false; } - public T? GetValue(CliDataSymbol dataSymbol) - => GetValueInternal(dataSymbol); + public T? GetValue(CliValueSymbol valueSymbol) + => GetValueInternal(valueSymbol); private T? GetValueInternal(CliSymbol? symbol) { @@ -53,9 +53,9 @@ not null when TryGetValue(symbol, out var value) // configuration values go here in precedence //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) // => UseValue(symbol, GetEnvByName(envName)), - not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation) + not null when symbol.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation) => UseValue(symbol, CalculatedDefault(symbol, (Func)defaultValueCalculation)), - not null when TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out T? explicitValue) + not null when symbol.TryGetAnnotation(ValueAnnotations.DefaultValue, out T? explicitValue) => UseValue(symbol, explicitValue), null => throw new ArgumentNullException(nameof(symbol)), _ => UseValue(symbol, default(T)) diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index b708448ffe..cba48dc98c 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -139,7 +139,7 @@ public void Option_short_forms_can_be_bundled() var result = CliParser.Parse(command, "the-command -xyz"); - result.CommandResult + result.CommandValueResult .ValueResults .Select(o => ((CliValueResult)o).ValueSymbol.Name) .Should() @@ -188,7 +188,7 @@ public void Option_long_forms_do_not_get_unbundled() var result = CliParser.Parse(command, "the-command --xyz"); - result.CommandResultInternal + result.CommandResult .Children .Select(o => ((CliOptionResultInternal)o).Option.Name) .Should() @@ -210,7 +210,7 @@ public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_v ParseResult result = CliParser.Parse(outer, "outer inner -abc"); - result.CommandResultInternal + result.CommandResult .Tokens .Select(t => t.Value) .Should() @@ -422,7 +422,7 @@ public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_foll .Should() .BeEquivalentTo("carrot"); - result.CommandResultInternal + result.CommandResult .Tokens .Select(t => t.Value) .Should() @@ -440,13 +440,13 @@ public void Command_with_multiple_options_is_parsed_correctly() var result = CliParser.Parse(command, "outer --inner1 argument1 --inner2 argument2"); - result.CommandResultInternal + result.CommandResult .Children .Should() .ContainSingle(o => ((CliOptionResultInternal)o).Option.Name == "--inner1" && o.Tokens.Single().Value == "argument1"); - result.CommandResultInternal + result.CommandResult .Children .Should() .ContainSingle(o => diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index e918f844a4..6ca564385b 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -52,8 +52,7 @@ internal ParseResult( Configuration = configuration; _rootCommandResult = rootCommandResult; // TODO: Why do we need this? - CommandResultInternal = commandResultInternal; - CommandResult = commandResultInternal.CommandResult; + CommandResult = commandResultInternal; this.valueResultDictionary = valueResultDictionary; // TODO: invocation /* @@ -102,22 +101,22 @@ internal ParseResult( /// /// A result indicating the command specified in the command line input. /// - internal CommandResult CommandResult { get; } + internal CliCommandResultInternal CommandResult { get; } - private CommandValueResult? commandValueResult = null; - public CommandValueResult CommandValueResult + private CliCommandResult? commandValueResult = null; + public CliCommandResult CommandValueResult { get { if (commandValueResult is null) { - commandValueResult = new CommandValueResult(CommandResult); + commandValueResult = new CliCommandResult(CommandResult.Command, CommandResult.Tokens.Select(t=>t.Location) ); } return commandValueResult; } } - public IEnumerable AllValueResults + public IEnumerable AllValueResults => valueResultDictionary.Values; diff --git a/src/System.CommandLine/Parsing/CliValueResult.cs b/src/System.CommandLine/Parsing/CliValueResult.cs index 8286dbc4e6..288398e97f 100644 --- a/src/System.CommandLine/Parsing/CliValueResult.cs +++ b/src/System.CommandLine/Parsing/CliValueResult.cs @@ -41,7 +41,7 @@ internal CliValueResult( ValueResultOutcome outcome, // TODO: Error should be an Enumerable and perhaps should not be here at all, only on ParseResult string? error = null) - : this((CliSymbol)argument, value, locations, outcome, error) + : this((CliValueSymbol)argument, value, locations, outcome, error) { } /// @@ -59,13 +59,13 @@ internal CliValueResult( ValueResultOutcome outcome, // TODO: Error should be an Enumerable and perhaps should not be here at all, only on ParseResult string? error = null) - : this((CliSymbol)option, value, locations, outcome, error) + : this((CliValueSymbol)option, value, locations, outcome, error) { } /// /// The CliSymbol the value is for. This is always a CliOption or CliArgument. /// - public CliDataSymbol ValueSymbol { get; } + public CliSymbol ValueSymbol { get; } internal object? Value { get; } diff --git a/src/System.CommandLine/Parsing/SymbolLookupByName.cs b/src/System.CommandLine/Parsing/SymbolLookupByName.cs index 414157c10d..877e8c5172 100644 --- a/src/System.CommandLine/Parsing/SymbolLookupByName.cs +++ b/src/System.CommandLine/Parsing/SymbolLookupByName.cs @@ -43,7 +43,7 @@ private List BuildCache(ParseResult parseResult) return cache; } cache = []; - var commandResult = parseResult.CommandResultInternal; + var commandResult = parseResult.CommandResult; while (commandResult is not null) { var command = commandResult.Command; From 27397298971fe798830c624451632129cc9492a9 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 17 Aug 2024 12:19:50 -0400 Subject: [PATCH 119/150] Updates to match main-powederhouse --- .../ValueSubsystemTests.cs | 19 +------- .../Directives/DiagramSubsystem.cs | 11 ++--- src/System.CommandLine.Subsystems/Pipeline.cs | 6 --- src/System.CommandLine.Tests/ParserTests.cs | 12 ++--- src/System.CommandLine/ParseResult.cs | 24 ++-------- .../Parsing/CliValueResult.cs | 47 +------------------ .../Parsing/SymbolLookupByName.cs | 2 +- 7 files changed, 19 insertions(+), 102 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index 4284562994..5181b903c9 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -10,25 +10,8 @@ namespace System.CommandLine.Subsystems.Tests; public class ValueSubsystemTests { - //[Fact] - //public void ValueSubsystem_is_activated_by_default() - //{ - // CliRootCommand rootCommand = [ - // new CliCommand("x") - // { - // new CliOption("--opt1") - // }]; - // var configuration = new CliConfiguration(rootCommand); - // var subsystem = new ValueProvider(); - // var input = "x --opt1 Kirk"; - // var args = CliParser.SplitCommandLine(input).ToList(); + // TODO: Add various default value tests - // Subsystem.Initialize(subsystem, configuration, args); - // var parseResult = CliParser.Parse(rootCommand, args[0], configuration); - // var isActive = Subsystem.GetIsActivated(subsystem, parseResult); - - // isActive.Should().BeTrue(); - //} /* Hold these tests until we determine if ValueSubsystem is replaceable [Fact] public void ValueSubsystem_returns_values_that_are_entered() diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 3984632bc0..b7443e9b7e 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -67,17 +67,17 @@ private static void Diagram( { builder.Append('!'); } - */ - // TODO: Directives - /* + */ + // TODO: Directives + /* switch (symbolResult) { case DirectiveResult { Directive: not DiagramDirective }: break; */ - // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives - /* + // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives + /* case ArgumentResult argumentResult: { var includeArgumentName = @@ -177,6 +177,5 @@ private static void Diagram( } } } -} */ } diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 3a568e4776..53bbb41760 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -56,7 +56,6 @@ public static Pipeline CreateEmpty() private Pipeline() { - //Value = new ValueProvider(); Response = new ResponseSubsystem(); Invocation = new InvocationSubsystem(); Validation = new ValidationSubsystem(); @@ -182,11 +181,6 @@ public ErrorReportingSubsystem? ErrorReporting /// public InvocationSubsystem? Invocation { get; } - ///// - ///// Gets the value subsystem which manages entered and default values. - ///// - //public ValueProvider Value { get; } - /// /// Gets the response file subsystem /// diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index cba48dc98c..b708448ffe 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -139,7 +139,7 @@ public void Option_short_forms_can_be_bundled() var result = CliParser.Parse(command, "the-command -xyz"); - result.CommandValueResult + result.CommandResult .ValueResults .Select(o => ((CliValueResult)o).ValueSymbol.Name) .Should() @@ -188,7 +188,7 @@ public void Option_long_forms_do_not_get_unbundled() var result = CliParser.Parse(command, "the-command --xyz"); - result.CommandResult + result.CommandResultInternal .Children .Select(o => ((CliOptionResultInternal)o).Option.Name) .Should() @@ -210,7 +210,7 @@ public void Options_do_not_get_unbundled_unless_all_resulting_options_would_be_v ParseResult result = CliParser.Parse(outer, "outer inner -abc"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -422,7 +422,7 @@ public void When_an_option_is_not_respecified_but_limit_is_reached_then_the_foll .Should() .BeEquivalentTo("carrot"); - result.CommandResult + result.CommandResultInternal .Tokens .Select(t => t.Value) .Should() @@ -440,13 +440,13 @@ public void Command_with_multiple_options_is_parsed_correctly() var result = CliParser.Parse(command, "outer --inner1 argument1 --inner2 argument2"); - result.CommandResult + result.CommandResultInternal .Children .Should() .ContainSingle(o => ((CliOptionResultInternal)o).Option.Name == "--inner1" && o.Tokens.Single().Value == "argument1"); - result.CommandResult + result.CommandResultInternal .Children .Should() .ContainSingle(o => diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 6ca564385b..5a245913e4 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -52,7 +52,8 @@ internal ParseResult( Configuration = configuration; _rootCommandResult = rootCommandResult; // TODO: Why do we need this? - CommandResult = commandResultInternal; + CommandResultInternal = commandResultInternal; + CommandResult = commandResultInternal.CommandResult; this.valueResultDictionary = valueResultDictionary; // TODO: invocation /* @@ -101,25 +102,10 @@ internal ParseResult( /// /// A result indicating the command specified in the command line input. /// - internal CliCommandResultInternal CommandResult { get; } - - private CliCommandResult? commandValueResult = null; - public CliCommandResult CommandValueResult - { - get - { - if (commandValueResult is null) - { - commandValueResult = new CliCommandResult(CommandResult.Command, CommandResult.Tokens.Select(t=>t.Location) ); - } - return commandValueResult; - } - } - - public IEnumerable AllValueResults - => valueResultDictionary.Values; - + // TODO: Update SymbolLookupByName to use CommandValueResult, then remove + internal CliCommandResultInternal CommandResultInternal { get; } + public CliCommandResult CommandResult { get; } /// /// The configuration used to produce the parse result. diff --git a/src/System.CommandLine/Parsing/CliValueResult.cs b/src/System.CommandLine/Parsing/CliValueResult.cs index 288398e97f..1b44e3e530 100644 --- a/src/System.CommandLine/Parsing/CliValueResult.cs +++ b/src/System.CommandLine/Parsing/CliValueResult.cs @@ -10,7 +10,7 @@ namespace System.CommandLine.Parsing; /// public class CliValueResult : CliSymbolResult { - private CliValueResult( + internal CliValueResult( CliValueSymbol valueSymbol, object? value, IEnumerable locations, @@ -26,42 +26,6 @@ private CliValueResult( Error = error; } - /// - /// Creates a new ValueResult instance - /// - /// The CliArgument the value is for. - /// The entered value. - /// The locations list. - /// True if parsing and converting the value was successful. - /// The CliError if parsing or converting failed, otherwise null. - internal CliValueResult( - CliArgument argument, - object? value, - IEnumerable locations, - ValueResultOutcome outcome, - // TODO: Error should be an Enumerable and perhaps should not be here at all, only on ParseResult - string? error = null) - : this((CliValueSymbol)argument, value, locations, outcome, error) - { } - - /// - /// Creates a new ValueResult instance - /// - /// The CliOption the value is for. - /// The entered value. - /// The locations list. - /// True if parsing and converting the value was successful. - /// The CliError if parsing or converting failed, otherwise null. - internal CliValueResult( - CliOption option, - object? value, - IEnumerable locations, - ValueResultOutcome outcome, - // TODO: Error should be an Enumerable and perhaps should not be here at all, only on ParseResult - string? error = null) - : this((CliValueSymbol)option, value, locations, outcome, error) - { } - /// /// The CliSymbol the value is for. This is always a CliOption or CliArgument. /// @@ -89,15 +53,6 @@ internal CliValueResult( ? default : Value; - - /// - /// Gets the locations at which the tokens that made up the value appeared. - /// - /// - /// This needs to be a collection because collection types have multiple tokens and they will not be simple offsets when response files are used. - /// - public IEnumerable Locations { get; } - /// /// True when parsing and converting the value was successful /// diff --git a/src/System.CommandLine/Parsing/SymbolLookupByName.cs b/src/System.CommandLine/Parsing/SymbolLookupByName.cs index 877e8c5172..414157c10d 100644 --- a/src/System.CommandLine/Parsing/SymbolLookupByName.cs +++ b/src/System.CommandLine/Parsing/SymbolLookupByName.cs @@ -43,7 +43,7 @@ private List BuildCache(ParseResult parseResult) return cache; } cache = []; - var commandResult = parseResult.CommandResult; + var commandResult = parseResult.CommandResultInternal; while (commandResult is not null) { var command = commandResult.Command; From c40ebbda51f68c4a0dc8bbc6ead35e32c056f0e5 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 17 Aug 2024 13:18:34 -0400 Subject: [PATCH 120/150] Removed gratuitous changes and fixed whitespace --- .../Directives/DiagramSubsystem.cs | 124 +++++------ src/System.CommandLine.Tests/ParserTests.cs | 194 ++++++++-------- src/System.CommandLine/CliArgument{T}.cs | 207 +++++++++--------- src/System.CommandLine/CliOption.cs | 1 + 4 files changed, 263 insertions(+), 263 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index b7443e9b7e..4e7dfb2ef9 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -76,78 +76,78 @@ private static void Diagram( break; */ - // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives - /* - case ArgumentResult argumentResult: - { - var includeArgumentName = - argumentResult.Argument.FirstParent!.Symbol is CliCommand { HasArguments: true, Arguments.Count: > 1 }; - - if (includeArgumentName) + // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives + /* + case ArgumentResult argumentResult: { - builder.Append("[ "); - builder.Append(argumentResult.Argument.Name); - builder.Append(' '); - } + var includeArgumentName = + argumentResult.Argument.FirstParent!.Symbol is CliCommand { HasArguments: true, Arguments.Count: > 1 }; - if (argumentResult.Argument.Arity.MaximumNumberOfValues > 0) - { - ArgumentConversionResult conversionResult = argumentResult.GetArgumentConversionResult(); - switch (conversionResult.Result) + if (includeArgumentName) { - case ArgumentConversionResultType.NoArgument: - break; - case ArgumentConversionResultType.Successful: - switch (conversionResult.Value) - { - case string s: - builder.Append($"<{s}>"); - break; - - case IEnumerable items: - builder.Append('<'); - builder.Append( - string.Join("> <", - items.Cast().ToArray())); - builder.Append('>'); - break; - - default: - builder.Append('<'); - builder.Append(conversionResult.Value); - builder.Append('>'); - break; - } - - break; - - default: // failures - builder.Append('<'); - builder.Append(string.Join("> <", symbolResult.Tokens.Select(t => t.Value))); - builder.Append('>'); - - break; + builder.Append("[ "); + builder.Append(argumentResult.Argument.Name); + builder.Append(' '); } - } - if (includeArgumentName) - { - builder.Append(" ]"); - } + if (argumentResult.Argument.Arity.MaximumNumberOfValues > 0) + { + ArgumentConversionResult conversionResult = argumentResult.GetArgumentConversionResult(); + switch (conversionResult.Result) + { + case ArgumentConversionResultType.NoArgument: + break; + case ArgumentConversionResultType.Successful: + switch (conversionResult.Value) + { + case string s: + builder.Append($"<{s}>"); + break; + + case IEnumerable items: + builder.Append('<'); + builder.Append( + string.Join("> <", + items.Cast().ToArray())); + builder.Append('>'); + break; + + default: + builder.Append('<'); + builder.Append(conversionResult.Value); + builder.Append('>'); + break; + } + + break; + + default: // failures + builder.Append('<'); + builder.Append(string.Join("> <", symbolResult.Tokens.Select(t => t.Value))); + builder.Append('>'); + + break; + } + } - break; - } + if (includeArgumentName) + { + builder.Append(" ]"); + } - default: - { - OptionResult? optionResult = symbolResult as OptionResult; + break; + } - if (optionResult is { Implicit: true }) + default: { - builder.Append('*'); - } + OptionResult? optionResult = symbolResult as OptionResult; + + if (optionResult is { Implicit: true }) + { + builder.Append('*'); + } - builder.Append("[ "); + builder.Append("[ "); if (optionResult is not null) { diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index b708448ffe..fffaa5b724 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -854,124 +854,124 @@ public void Absolute_Windows_style_paths_are_lexed_correctly() } /* These tests should be split and those using an explicit default value moved to subsystem, and those using the type default should remain in core (?). This might not be meaningful if the type conversion is correct. What value other than the type default could be used. - [Fact] - public void Commands_can_have_default_argument_values() - { - var argument = new CliArgument("the-arg") - { - DefaultValueFactory = (_) => "default" - }; + [Fact] + public void Commands_can_have_default_argument_values() + { + var argument = new CliArgument("the-arg") + { + DefaultValueFactory = (_) => "default" + }; - var command = new CliCommand("command") - { - argument - }; + var command = new CliCommand("command") + { + argument + }; - ParseResult result = CliParser.Parse(command, "command"); + ParseResult result = CliParser.Parse(command, "command"); - GetValue(result, argument) - .Should() - .Be("default"); - } + GetValue(result, argument) + .Should() + .Be("default"); + } - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_the_option_can_still_be_accessed_as_though_it_had_been_applied() - { - var command = new CliCommand("command"); - var option = new CliOption("-o", "--option") - { - DefaultValueFactory = (_) => "the-default" - }; - command.Options.Add(option); + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_the_option_can_still_be_accessed_as_though_it_had_been_applied() + { + var command = new CliCommand("command"); + var option = new CliOption("-o", "--option") + { + DefaultValueFactory = (_) => "the-default" + }; + command.Options.Add(option); - ParseResult result = CliParser.Parse(command, "command"); + ParseResult result = CliParser.Parse(command, "command"); - result.GetResult(option).Should().NotBeNull(); - GetValue(result, option).Should().Be("the-default"); - } + result.GetResult(option).Should().NotBeNull(); + GetValue(result, option).Should().Be("the-default"); + } - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_the_option_result_is_implicit() - { - var option = new CliOption("-o", "--option") - { - DefaultValueFactory = (_) => "the-default" - }; + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_the_option_result_is_implicit() + { + var option = new CliOption("-o", "--option") + { + DefaultValueFactory = (_) => "the-default" + }; - var command = new CliCommand("command") - { - option - }; + var command = new CliCommand("command") + { + option + }; - var result = CliParser.Parse(command, "command"); + var result = CliParser.Parse(command, "command"); - result.GetResult(option) - .Implicit - .Should() - .BeTrue(); - } + result.GetResult(option) + .Implicit + .Should() + .BeTrue(); + } - [Fact] - public void When_an_option_with_a_default_value_is_not_matched_then_there_are_no_tokens() - { - var option = new CliOption("-o") - { - DefaultValueFactory = (_) => "the-default" - }; + [Fact] + public void When_an_option_with_a_default_value_is_not_matched_then_there_are_no_tokens() + { + var option = new CliOption("-o") + { + DefaultValueFactory = (_) => "the-default" + }; - var command = new CliCommand("command") - { - option - }; + var command = new CliCommand("command") + { + option + }; - var result = CliParser.Parse(command, "command"); + var result = CliParser.Parse(command, "command"); - result.GetResult(option) - .IdentifierToken - .Should() - .BeEquivalentTo(default(CliToken)); - } + result.GetResult(option) + .IdentifierToken + .Should() + .BeEquivalentTo(default(CliToken)); + } - [Fact] - public void When_an_argument_with_a_default_value_is_not_matched_then_there_are_no_tokens() - { - var argument = new CliArgument("o") - { - DefaultValueFactory = (_) => "the-default" - }; + [Fact] + public void When_an_argument_with_a_default_value_is_not_matched_then_there_are_no_tokens() + { + var argument = new CliArgument("o") + { + DefaultValueFactory = (_) => "the-default" + }; - var command = new CliCommand("command") - { - argument - }; - var result = CliParser.Parse(command, "command"); + var command = new CliCommand("command") + { + argument + }; + var result = CliParser.Parse(command, "command"); - result.GetResult(argument) - .Tokens - .Should() - .BeEmpty(); - } + result.GetResult(argument) + .Tokens + .Should() + .BeEmpty(); + } - [Fact] - public void Command_default_argument_value_does_not_override_parsed_value() - { - var argument = new CliArgument("the-arg") - { - DefaultValueFactory = (_) => new DirectoryInfo(Directory.GetCurrentDirectory()) - }; + [Fact] + public void Command_default_argument_value_does_not_override_parsed_value() + { + var argument = new CliArgument("the-arg") + { + DefaultValueFactory = (_) => new DirectoryInfo(Directory.GetCurrentDirectory()) + }; - var command = new CliCommand("inner") - { - argument - }; + var command = new CliCommand("inner") + { + argument + }; - var result = CliParser.Parse(command, "the-directory"); + var result = CliParser.Parse(command, "the-directory"); - GetValue(result, argument) - .Name - .Should() - .Be("the-directory"); - } + GetValue(result, argument) + .Name + .Should() + .Be("the-directory"); + } */ [Fact] diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index a1949a3b0a..f5d6158ce3 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -11,17 +11,16 @@ namespace System.CommandLine /// public class CliArgument : CliArgument { -// TODO: custom parser -/* - private Func? _customParser; -*/ + // TODO: custom parser + /* + private Func? _customParser; + */ /// /// Initializes a new instance of the Argument class. /// /// The name of the argument. It's not used for parsing, only when displaying Help or creating parse errors.> /// - public CliArgument(string name) - : base(name) + public CliArgument(string name) : base(name) { } @@ -37,45 +36,45 @@ public CliArgument(string name) */ internal Func? DefaultValueFactory { get; set; } -// TODO: custom parsers -/* - /// - /// A custom argument parser. - /// - /// - /// It's invoked when there was parse input provided for given Argument. - /// The same instance can be set as , in such case - /// the delegate is also invoked when no input was provided. - /// - public Func? CustomParser - { - get => _customParser; - set - { - _customParser = value; - - if (value is not null) + // TODO: custom parsers + /* + /// + /// A custom argument parser. + /// + /// + /// It's invoked when there was parse input provided for given Argument. + /// The same instance can be set as , in such case + /// the delegate is also invoked when no input was provided. + /// + public Func? CustomParser { - ConvertArguments = (ArgumentResult argumentResult, out object? parsedValue) => + get => _customParser; + set { - int errorsBefore = argumentResult.SymbolResultTree.ErrorCount; - var result = value(argumentResult); + _customParser = value; - if (errorsBefore == argumentResult.SymbolResultTree.ErrorCount) + if (value is not null) { - parsedValue = result; - return true; + ConvertArguments = (ArgumentResult argumentResult, out object? parsedValue) => + { + int errorsBefore = argumentResult.SymbolResultTree.ErrorCount; + var result = value(argumentResult); + + if (errorsBefore == argumentResult.SymbolResultTree.ErrorCount) + { + parsedValue = result; + return true; + } + else + { + parsedValue = default(T)!; + return false; + } + }; } - else - { - parsedValue = default(T)!; - return false; - } - }; + } } - } - } -*/ + */ /// public override Type ValueType => typeof(T); @@ -91,87 +90,87 @@ public CliArgument(string name) return DefaultValueFactory.Invoke(argumentResult); } -// TODO: completion, validators -/* - /// - /// Configures the argument to accept only the specified values, and to suggest them as command line completions. - /// - /// The values that are allowed for the argument. - public void AcceptOnlyFromAmong(params string[] values) - { - if (values is not null && values.Length > 0) - { - Validators.Clear(); - Validators.Add(UnrecognizedArgumentError); - CompletionSources.Clear(); - CompletionSources.Add(values); - } - - void UnrecognizedArgumentError(ArgumentResult argumentResult) - { - for (var i = 0; i < argumentResult.Tokens.Count; i++) + // TODO: completion, validators + /* + /// + /// Configures the argument to accept only the specified values, and to suggest them as command line completions. + /// + /// The values that are allowed for the argument. + public void AcceptOnlyFromAmong(params string[] values) { - var token = argumentResult.Tokens[i]; + if (values is not null && values.Length > 0) + { + Validators.Clear(); + Validators.Add(UnrecognizedArgumentError); + CompletionSources.Clear(); + CompletionSources.Add(values); + } - if (token.Symbol is null || token.Symbol == this) + void UnrecognizedArgumentError(ArgumentResult argumentResult) { - if (Array.IndexOf(values, token.Value) < 0) + for (var i = 0; i < argumentResult.Tokens.Count; i++) { - argumentResult.AddError(LocalizationResources.UnrecognizedArgument(token.Value, values)); + var token = argumentResult.Tokens[i]; + + if (token.Symbol is null || token.Symbol == this) + { + if (Array.IndexOf(values, token.Value) < 0) + { + argumentResult.AddError(LocalizationResources.UnrecognizedArgument(token.Value, values)); + } + } } } } - } - } - /// - /// Configures the argument to accept only values representing legal file paths. - /// - public void AcceptLegalFilePathsOnly() - { - Validators.Add(static result => - { - var invalidPathChars = Path.GetInvalidPathChars(); - - for (var i = 0; i < result.Tokens.Count; i++) + /// + /// Configures the argument to accept only values representing legal file paths. + /// + public void AcceptLegalFilePathsOnly() { - var token = result.Tokens[i]; + Validators.Add(static result => + { + var invalidPathChars = Path.GetInvalidPathChars(); - // File class no longer check invalid character - // https://blogs.msdn.microsoft.com/jeremykuhne/2018/03/09/custom-directory-enumeration-in-net-core-2-1/ - var invalidCharactersIndex = token.Value.IndexOfAny(invalidPathChars); + for (var i = 0; i < result.Tokens.Count; i++) + { + var token = result.Tokens[i]; - if (invalidCharactersIndex >= 0) - { - result.AddError(LocalizationResources.InvalidCharactersInPath(token.Value[invalidCharactersIndex])); - } - } - }); - } + // File class no longer check invalid character + // https://blogs.msdn.microsoft.com/jeremykuhne/2018/03/09/custom-directory-enumeration-in-net-core-2-1/ + var invalidCharactersIndex = token.Value.IndexOfAny(invalidPathChars); - /// - /// Configures the argument to accept only values representing legal file names. - /// - /// A parse error will result, for example, if file path separators are found in the parsed value. - public void AcceptLegalFileNamesOnly() - { - Validators.Add(static result => - { - var invalidFileNameChars = Path.GetInvalidFileNameChars(); + if (invalidCharactersIndex >= 0) + { + result.AddError(LocalizationResources.InvalidCharactersInPath(token.Value[invalidCharactersIndex])); + } + } + }); + } - for (var i = 0; i < result.Tokens.Count; i++) + /// + /// Configures the argument to accept only values representing legal file names. + /// + /// A parse error will result, for example, if file path separators are found in the parsed value. + public void AcceptLegalFileNamesOnly() { - var token = result.Tokens[i]; - var invalidCharactersIndex = token.Value.IndexOfAny(invalidFileNameChars); - - if (invalidCharactersIndex >= 0) + Validators.Add(static result => { - result.AddError(LocalizationResources.InvalidCharactersInFileName(token.Value[invalidCharactersIndex])); - } + var invalidFileNameChars = Path.GetInvalidFileNameChars(); + + for (var i = 0; i < result.Tokens.Count; i++) + { + var token = result.Tokens[i]; + var invalidCharactersIndex = token.Value.IndexOfAny(invalidFileNameChars); + + if (invalidCharactersIndex >= 0) + { + result.AddError(LocalizationResources.InvalidCharactersInFileName(token.Value[invalidCharactersIndex])); + } + } + }); } - }); - } -*/ + */ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "https://github.com/dotnet/command-line-api/issues/1638")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2091", Justification = "https://github.com/dotnet/command-line-api/issues/1638")] diff --git a/src/System.CommandLine/CliOption.cs b/src/System.CommandLine/CliOption.cs index fc9bdabe9b..ae0a9b7ebf 100644 --- a/src/System.CommandLine/CliOption.cs +++ b/src/System.CommandLine/CliOption.cs @@ -15,6 +15,7 @@ public abstract class CliOption : CliValueSymbol internal AliasSet? _aliases; /* private List>? _validators; + */ private protected CliOption(string name, string[] aliases) From 7cd30d7fa03650f62c6933fbc173aae1db1b22bc Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 17 Aug 2024 13:28:39 -0400 Subject: [PATCH 121/150] More whitespace --- .../Directives/DiagramSubsystem.cs | 18 +- src/System.CommandLine/CliArgument{T}.cs | 184 +++++++++--------- 2 files changed, 101 insertions(+), 101 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 4e7dfb2ef9..796c7e7363 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -68,16 +68,15 @@ private static void Diagram( builder.Append('!'); } */ - // TODO: Directives - /* + + /* Directives switch (symbolResult) { case DirectiveResult { Directive: not DiagramDirective }: break; */ - // TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives - /* + /* TODO: This logic is deeply tied to internal types/properties. These aren't things we probably want to expose like SymbolNode. See #2349 for alternatives case ArgumentResult argumentResult: { var includeArgumentName = @@ -167,13 +166,14 @@ private static void Diagram( continue; } - builder.Append(' '); + builder.Append(' '); - Diagram(builder, child, parseResult); - } + Diagram(builder, child, parseResult); + } - builder.Append(" ]"); - break; + builder.Append(" ]"); + break; + } } } } diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index f5d6158ce3..60d72d4bad 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -38,42 +38,42 @@ public CliArgument(string name) : base(name) // TODO: custom parsers /* - /// - /// A custom argument parser. - /// - /// - /// It's invoked when there was parse input provided for given Argument. - /// The same instance can be set as , in such case - /// the delegate is also invoked when no input was provided. - /// - public Func? CustomParser + /// + /// A custom argument parser. + /// + /// + /// It's invoked when there was parse input provided for given Argument. + /// The same instance can be set as , in such case + /// the delegate is also invoked when no input was provided. + /// + public Func? CustomParser + { + get => _customParser; + set + { + _customParser = value; + + if (value is not null) { - get => _customParser; - set + ConvertArguments = (ArgumentResult argumentResult, out object? parsedValue) => { - _customParser = value; + int errorsBefore = argumentResult.SymbolResultTree.ErrorCount; + var result = value(argumentResult); - if (value is not null) + if (errorsBefore == argumentResult.SymbolResultTree.ErrorCount) { - ConvertArguments = (ArgumentResult argumentResult, out object? parsedValue) => - { - int errorsBefore = argumentResult.SymbolResultTree.ErrorCount; - var result = value(argumentResult); - - if (errorsBefore == argumentResult.SymbolResultTree.ErrorCount) - { - parsedValue = result; - return true; - } - else - { - parsedValue = default(T)!; - return false; - } - }; + parsedValue = result; + return true; } - } + else + { + parsedValue = default(T)!; + return false; + } + }; } + } + } */ /// public override Type ValueType => typeof(T); @@ -92,84 +92,84 @@ public CliArgument(string name) : base(name) } // TODO: completion, validators /* - /// - /// Configures the argument to accept only the specified values, and to suggest them as command line completions. - /// - /// The values that are allowed for the argument. - public void AcceptOnlyFromAmong(params string[] values) + /// + /// Configures the argument to accept only the specified values, and to suggest them as command line completions. + /// + /// The values that are allowed for the argument. + public void AcceptOnlyFromAmong(params string[] values) + { + if (values is not null && values.Length > 0) + { + Validators.Clear(); + Validators.Add(UnrecognizedArgumentError); + CompletionSources.Clear(); + CompletionSources.Add(values); + } + + void UnrecognizedArgumentError(ArgumentResult argumentResult) + { + for (var i = 0; i < argumentResult.Tokens.Count; i++) { - if (values is not null && values.Length > 0) - { - Validators.Clear(); - Validators.Add(UnrecognizedArgumentError); - CompletionSources.Clear(); - CompletionSources.Add(values); - } + var token = argumentResult.Tokens[i]; - void UnrecognizedArgumentError(ArgumentResult argumentResult) + if (token.Symbol is null || token.Symbol == this) { - for (var i = 0; i < argumentResult.Tokens.Count; i++) + if (Array.IndexOf(values, token.Value) < 0) { - var token = argumentResult.Tokens[i]; - - if (token.Symbol is null || token.Symbol == this) - { - if (Array.IndexOf(values, token.Value) < 0) - { - argumentResult.AddError(LocalizationResources.UnrecognizedArgument(token.Value, values)); - } - } + argumentResult.AddError(LocalizationResources.UnrecognizedArgument(token.Value, values)); } } } + } + } - /// - /// Configures the argument to accept only values representing legal file paths. - /// - public void AcceptLegalFilePathsOnly() - { - Validators.Add(static result => - { - var invalidPathChars = Path.GetInvalidPathChars(); + /// + /// Configures the argument to accept only values representing legal file paths. + /// + public void AcceptLegalFilePathsOnly() + { + Validators.Add(static result => + { + var invalidPathChars = Path.GetInvalidPathChars(); - for (var i = 0; i < result.Tokens.Count; i++) - { - var token = result.Tokens[i]; + for (var i = 0; i < result.Tokens.Count; i++) + { + var token = result.Tokens[i]; - // File class no longer check invalid character - // https://blogs.msdn.microsoft.com/jeremykuhne/2018/03/09/custom-directory-enumeration-in-net-core-2-1/ - var invalidCharactersIndex = token.Value.IndexOfAny(invalidPathChars); + // File class no longer check invalid character + // https://blogs.msdn.microsoft.com/jeremykuhne/2018/03/09/custom-directory-enumeration-in-net-core-2-1/ + var invalidCharactersIndex = token.Value.IndexOfAny(invalidPathChars); - if (invalidCharactersIndex >= 0) - { - result.AddError(LocalizationResources.InvalidCharactersInPath(token.Value[invalidCharactersIndex])); - } - } - }); + if (invalidCharactersIndex >= 0) + { + result.AddError(LocalizationResources.InvalidCharactersInPath(token.Value[invalidCharactersIndex])); + } } + }); + } - /// - /// Configures the argument to accept only values representing legal file names. - /// - /// A parse error will result, for example, if file path separators are found in the parsed value. - public void AcceptLegalFileNamesOnly() - { - Validators.Add(static result => - { - var invalidFileNameChars = Path.GetInvalidFileNameChars(); + /// + /// Configures the argument to accept only values representing legal file names. + /// + /// A parse error will result, for example, if file path separators are found in the parsed value. + public void AcceptLegalFileNamesOnly() + { + Validators.Add(static result => + { + var invalidFileNameChars = Path.GetInvalidFileNameChars(); - for (var i = 0; i < result.Tokens.Count; i++) - { - var token = result.Tokens[i]; - var invalidCharactersIndex = token.Value.IndexOfAny(invalidFileNameChars); + for (var i = 0; i < result.Tokens.Count; i++) + { + var token = result.Tokens[i]; + var invalidCharactersIndex = token.Value.IndexOfAny(invalidFileNameChars); - if (invalidCharactersIndex >= 0) - { - result.AddError(LocalizationResources.InvalidCharactersInFileName(token.Value[invalidCharactersIndex])); - } - } - }); + if (invalidCharactersIndex >= 0) + { + result.AddError(LocalizationResources.InvalidCharactersInFileName(token.Value[invalidCharactersIndex])); + } } + }); + } */ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "https://github.com/dotnet/command-line-api/issues/1638")] From 1a875a28d33129ea8e8ade95d6c12f6bf0c661ac Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 17 Aug 2024 13:32:22 -0400 Subject: [PATCH 122/150] And more whitespace --- src/System.CommandLine/CliArgument{T}.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index 60d72d4bad..14015b7ac5 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -13,7 +13,7 @@ public class CliArgument : CliArgument { // TODO: custom parser /* - private Func? _customParser; + private Func? _customParser; */ /// /// Initializes a new instance of the Argument class. @@ -38,7 +38,7 @@ public CliArgument(string name) : base(name) // TODO: custom parsers /* - /// + /// /// A custom argument parser. /// /// From ce616d73790cd1dcbc7c6225bcceba007c28da24 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 17 Aug 2024 14:47:26 -0400 Subject: [PATCH 123/150] Fixed XML comment caught by CI --- src/System.CommandLine/Parsing/CliValueResult.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/System.CommandLine/Parsing/CliValueResult.cs b/src/System.CommandLine/Parsing/CliValueResult.cs index 1b44e3e530..1bcce4e7b2 100644 --- a/src/System.CommandLine/Parsing/CliValueResult.cs +++ b/src/System.CommandLine/Parsing/CliValueResult.cs @@ -46,7 +46,6 @@ internal CliValueResult( /// /// Returns the value, or the default for the type. /// - /// The type to return /// The value, cast to the requested type. public object? GetValue() => Value is null From 1093825ec04d426f977e7c0a7d1acd42fd4ce1a3 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 17 Aug 2024 16:16:49 -0400 Subject: [PATCH 124/150] Added GetValue to PipelineResults and added tests Not sure where along the way GetValue got lost :( --- .../ValueSubsystemTests.cs | 50 +++++++++++++++++++ .../PipelineResult.cs | 11 ++++ src/System.CommandLine/ParseResult.cs | 14 ++---- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index 5181b903c9..40821cac5b 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -5,11 +5,61 @@ using System.CommandLine.Directives; using System.CommandLine.Parsing; using Xunit; +using static System.CommandLine.Subsystems.Tests.TestData; namespace System.CommandLine.Subsystems.Tests; public class ValueSubsystemTests { + [Fact] + public void Values_that_are_entered_are_retrieved() + { + var option = new CliOption("--intOpt"); + var rootCommand = new CliRootCommand { option }; + var configuration = new CliConfiguration(rootCommand); + var pipeline = Pipeline.Create(); + var consoleHack = new ConsoleHack().RedirectToBuffer(true); + var input = "--intOpt 42"; + + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var pipelineResult = new PipelineResult(parseResult, input, pipeline); + + pipelineResult.Should().NotBeNull(); + var optionValueResult = pipelineResult.GetValueResult(option); + var optionValue = pipelineResult.GetValue(option); + optionValueResult.Should().NotBeNull(); + optionValue.Should().Be(42); + } + + [Fact] + public void Values_that_are_not_entered_are_type_default_with_no_default_values() + { + var stringOption = new CliOption("--stringOption"); + var intOption = new CliOption("--intOption"); + var dateOption = new CliOption("--dateOption"); + var nullableIntOption = new CliOption("--nullableIntOption"); + var guidOption = new CliOption("--guidOption"); + var rootCommand = new CliRootCommand { stringOption, intOption, dateOption, nullableIntOption, guidOption }; + var configuration = new CliConfiguration(rootCommand); + var pipeline = Pipeline.Create(); + var input = ""; + + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var pipelineResult = new PipelineResult(parseResult, input, pipeline); + + pipelineResult.Should().NotBeNull(); + var stringOptionValue = pipelineResult.GetValue(stringOption); + var intOptionValue = pipelineResult.GetValue(intOption); + var dateOptionValue = pipelineResult.GetValue(dateOption); + var nullableIntOptionValue = pipelineResult.GetValue(nullableIntOption); + var guidOptionValue = pipelineResult.GetValue(guidOption); + stringOptionValue.Should().BeNull(); + intOptionValue.Should().Be(0); + dateOptionValue.Should().Be(DateTime.MinValue); + nullableIntOptionValue.Should().BeNull(); + guidOptionValue.Should().Be(Guid.Empty); + } + // TODO: Add various default value tests /* Hold these tests until we determine if ValueSubsystem is replaceable diff --git a/src/System.CommandLine.Subsystems/PipelineResult.cs b/src/System.CommandLine.Subsystems/PipelineResult.cs index a5482794d2..22f8d2364f 100644 --- a/src/System.CommandLine.Subsystems/PipelineResult.cs +++ b/src/System.CommandLine.Subsystems/PipelineResult.cs @@ -7,6 +7,7 @@ namespace System.CommandLine; public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) { + // TODO: Try to build workflow so it is illegal to create this without a ParseResult private readonly List errors = []; public ParseResult? ParseResult { get; } = parseResult; private ValueProvider valueProvider { get; } = new ValueProvider(parseResult); @@ -19,6 +20,16 @@ public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? public bool AlreadyHandled { get; set; } public int ExitCode { get; set; } + public T? GetValue(CliValueSymbol dataSymbol) + => valueProvider.GetValue(dataSymbol); + + public object? GetValue(CliValueSymbol option) + => valueProvider.GetValue(option); + + public CliValueResult GetValueResult(CliValueSymbol dataSymbol) + => parseResult.GetValueResult(dataSymbol); + + public void AddErrors(IEnumerable errors) { if (errors is not null) diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 5a245913e4..12eaf67702 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -203,20 +203,12 @@ CommandLineText is null */ /// - /// Gets the ValueResult, if any, for the specified option. + /// Gets the ValueResult, if any, for the specified option or argument /// /// The option for which to find a result. /// A result for the specified option, or if it was not entered by the user. - public CliValueResult? GetValueResult(CliOption option) - => GetValueResultInternal(option); - - /// - /// Gets the result, if any, for the specified argument. - /// - /// The argument for which to find a result. - /// A result for the specified argument, or if it was not entered by the user. - public CliValueResult? GetValueResult(CliArgument argument) - => GetValueResultInternal(argument); + public CliValueResult? GetValueResult(CliValueSymbol valueSymbol) + => GetValueResultInternal(valueSymbol); private CliValueResult? GetValueResultInternal(CliSymbol symbol) => valueResultDictionary.TryGetValue(symbol, out var result) From 3d978085eaa6e6dbd8dd9dd61161520d09689eef Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 17 Aug 2024 16:21:18 -0400 Subject: [PATCH 125/150] Fixed CI issues with Xml comments and some NRT issues --- src/System.CommandLine.Subsystems/PipelineResult.cs | 8 ++++---- src/System.CommandLine/ParseResult.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/System.CommandLine.Subsystems/PipelineResult.cs b/src/System.CommandLine.Subsystems/PipelineResult.cs index 22f8d2364f..733e4110fd 100644 --- a/src/System.CommandLine.Subsystems/PipelineResult.cs +++ b/src/System.CommandLine.Subsystems/PipelineResult.cs @@ -5,11 +5,11 @@ namespace System.CommandLine; -public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) +public class PipelineResult(ParseResult parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) { // TODO: Try to build workflow so it is illegal to create this without a ParseResult private readonly List errors = []; - public ParseResult? ParseResult { get; } = parseResult; + public ParseResult ParseResult { get; } = parseResult; private ValueProvider valueProvider { get; } = new ValueProvider(parseResult); public string RawInput { get; } = rawInput; @@ -26,8 +26,8 @@ public class PipelineResult(ParseResult? parseResult, string rawInput, Pipeline? public object? GetValue(CliValueSymbol option) => valueProvider.GetValue(option); - public CliValueResult GetValueResult(CliValueSymbol dataSymbol) - => parseResult.GetValueResult(dataSymbol); + public CliValueResult? GetValueResult(CliValueSymbol dataSymbol) + => ParseResult.GetValueResult(dataSymbol); public void AddErrors(IEnumerable errors) diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 12eaf67702..2930f60d9b 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -205,7 +205,7 @@ CommandLineText is null /// /// Gets the ValueResult, if any, for the specified option or argument /// - /// The option for which to find a result. + /// The option or argument for which to find a result. /// A result for the specified option, or if it was not entered by the user. public CliValueResult? GetValueResult(CliValueSymbol valueSymbol) => GetValueResultInternal(valueSymbol); From 4f6931d6ae3bee44cdc149e2436de05e6328a3b2 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Mon, 19 Aug 2024 15:50:00 -0400 Subject: [PATCH 126/150] Update and reorganize compat code --- .../DynamicallyAccessedMemberTypes.cs | 2 +- .../DynamicallyAccessedMembersAttribute.cs | 2 +- .../CodeAnalysis/NullabilityAttributes.cs | 149 ++++++++++++++++++ .../CodeAnalysis/StringSyntaxAttribute.cs | 71 +++++++++ .../UnconditionalSuppressMessageAttribute.cs | 2 +- .../CompilerServices}/IsExternalInit.cs | 6 +- .../System.CommandLine.Extended.csproj | 3 +- .../System.CommandLine.Generator.csproj | 2 +- .../System.CommandLine.Subsystems.csproj | 4 + .../System.CommandLine.csproj | 3 +- src/System.Diagnostics.CodeAnalysis.cs | 74 --------- 11 files changed, 235 insertions(+), 83 deletions(-) rename src/{System.CommandLine/System.Diagnostics.CodeAnalysis => Common/Compat/CodeAnalysis}/DynamicallyAccessedMemberTypes.cs (99%) rename src/{System.CommandLine/System.Diagnostics.CodeAnalysis => Common/Compat/CodeAnalysis}/DynamicallyAccessedMembersAttribute.cs (99%) create mode 100644 src/Common/Compat/CodeAnalysis/NullabilityAttributes.cs create mode 100644 src/Common/Compat/CodeAnalysis/StringSyntaxAttribute.cs rename src/{System.CommandLine/System.Diagnostics.CodeAnalysis => Common/Compat/CodeAnalysis}/UnconditionalSuppressMessageAttribute.cs (99%) rename src/{System.CommandLine/System.Runtime.CompilerServices => Common/Compat/CompilerServices}/IsExternalInit.cs (88%) delete mode 100644 src/System.Diagnostics.CodeAnalysis.cs diff --git a/src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMemberTypes.cs b/src/Common/Compat/CodeAnalysis/DynamicallyAccessedMemberTypes.cs similarity index 99% rename from src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMemberTypes.cs rename to src/Common/Compat/CodeAnalysis/DynamicallyAccessedMemberTypes.cs index a139bdc050..a077f34c51 100644 --- a/src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMemberTypes.cs +++ b/src/Common/Compat/CodeAnalysis/DynamicallyAccessedMemberTypes.cs @@ -1,6 +1,6 @@ // adapted from: https://github.com/dotnet/aspnetcore/blob/404d81767784552b0a148cb8c437332ebe726ae9/src/Shared/CodeAnalysis/DynamicallyAccessedMemberTypes.cs -#if !NET6_0_OR_GREATER +#if !NET5_0_OR_GREATER // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. diff --git a/src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMembersAttribute.cs b/src/Common/Compat/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs similarity index 99% rename from src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMembersAttribute.cs rename to src/Common/Compat/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs index a39283b0ab..8a15e0106f 100644 --- a/src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMembersAttribute.cs +++ b/src/Common/Compat/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs @@ -1,6 +1,6 @@ // adapted from: https://github.com/dotnet/aspnetcore/blob/404d81767784552b0a148cb8c437332ebe726ae9/src/Shared/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs#L29 -#if !NET6_0_OR_GREATER +#if !NET5_0_OR_GREATER // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. diff --git a/src/Common/Compat/CodeAnalysis/NullabilityAttributes.cs b/src/Common/Compat/CodeAnalysis/NullabilityAttributes.cs new file mode 100644 index 0000000000..eaa568afac --- /dev/null +++ b/src/Common/Compat/CodeAnalysis/NullabilityAttributes.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ +#if !NETCOREAPP3_0_OR_GREATER + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class DisallowNullAttribute : Attribute + { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute + { } + + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage (AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute (bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage (AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute (bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage (AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute (string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage (AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage (AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute (bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } + +#endif +#if !NET5_0_OR_GREATER + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage (AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute (string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute (params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage (AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute (bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute (bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } +#endif +} \ No newline at end of file diff --git a/src/Common/Compat/CodeAnalysis/StringSyntaxAttribute.cs b/src/Common/Compat/CodeAnalysis/StringSyntaxAttribute.cs new file mode 100644 index 0000000000..ee4bfcf8c4 --- /dev/null +++ b/src/Common/Compat/CodeAnalysis/StringSyntaxAttribute.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ +#if !NET7_0_OR_GREATER + /// Specifies the syntax used in a string. + [AttributeUsage (AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + sealed class StringSyntaxAttribute : Attribute + { + /// Initializes the with the identifier of the syntax used. + /// The syntax identifier. + public StringSyntaxAttribute(string syntax) + { + Syntax = syntax; + Arguments = Array.Empty(); + } + + /// Initializes the with the identifier of the syntax used. + /// The syntax identifier. + /// Optional arguments associated with the specific syntax employed. + public StringSyntaxAttribute(string syntax, params object?[] arguments) + { + Syntax = syntax; + Arguments = arguments; + } + + /// Gets the identifier of the syntax used. + public string Syntax { get; } + + /// Optional arguments associated with the specific syntax employed. + public object?[] Arguments { get; } + + /// The syntax identifier for strings containing composite formats for string formatting. + public const string CompositeFormat = nameof(CompositeFormat); + + /// The syntax identifier for strings containing date format specifiers. + public const string DateOnlyFormat = nameof(DateOnlyFormat); + + /// The syntax identifier for strings containing date and time format specifiers. + public const string DateTimeFormat = nameof(DateTimeFormat); + + /// The syntax identifier for strings containing format specifiers. + public const string EnumFormat = nameof(EnumFormat); + + /// The syntax identifier for strings containing format specifiers. + public const string GuidFormat = nameof(GuidFormat); + + /// The syntax identifier for strings containing JavaScript Object Notation (JSON). + public const string Json = nameof(Json); + + /// The syntax identifier for strings containing numeric format specifiers. + public const string NumericFormat = nameof(NumericFormat); + + /// The syntax identifier for strings containing regular expressions. + public const string Regex = nameof(Regex); + + /// The syntax identifier for strings containing time format specifiers. + public const string TimeOnlyFormat = nameof(TimeOnlyFormat); + + /// The syntax identifier for strings containing format specifiers. + public const string TimeSpanFormat = nameof(TimeSpanFormat); + + /// The syntax identifier for strings containing URIs. + public const string Uri = nameof(Uri); + + /// The syntax identifier for strings containing XML. + public const string Xml = nameof(Xml); + } +#endif +} \ No newline at end of file diff --git a/src/System.CommandLine/System.Diagnostics.CodeAnalysis/UnconditionalSuppressMessageAttribute.cs b/src/Common/Compat/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs similarity index 99% rename from src/System.CommandLine/System.Diagnostics.CodeAnalysis/UnconditionalSuppressMessageAttribute.cs rename to src/Common/Compat/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs index 8c54ed2c91..9654691e50 100644 --- a/src/System.CommandLine/System.Diagnostics.CodeAnalysis/UnconditionalSuppressMessageAttribute.cs +++ b/src/Common/Compat/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs @@ -1,6 +1,6 @@ // adapted from: https://github.com/dotnet/runtime/blob/a5159b1a8840632ad34cf59c5aaf77040cb6ceda/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs#L21 -#if !NET6_0_OR_GREATER +#if !NET5_0_OR_GREATER // Licensed to the .NET Foundation under one or more agreements. diff --git a/src/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs b/src/Common/Compat/CompilerServices/IsExternalInit.cs similarity index 88% rename from src/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs rename to src/Common/Compat/CompilerServices/IsExternalInit.cs index aee0d3ca7e..362fa00c5d 100644 --- a/src/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs +++ b/src/Common/Compat/CompilerServices/IsExternalInit.cs @@ -1,8 +1,12 @@ // 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. +#if !NET5_0_OR_GREATER + namespace System.Runtime.CompilerServices; internal static class IsExternalInit { -} \ No newline at end of file +} + +#endif \ No newline at end of file diff --git a/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj b/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj index d52c79f48f..fc75683c8e 100644 --- a/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj +++ b/src/System.CommandLine.Extended/System.CommandLine.Extended.csproj @@ -23,9 +23,8 @@ - - + diff --git a/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj b/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj index ba581bf1b5..9ba2dbb923 100644 --- a/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj +++ b/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj b/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj index b039b50e5d..8416c9e173 100644 --- a/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj +++ b/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 16229d529e..841860250d 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -23,7 +23,6 @@ - @@ -66,7 +65,7 @@ - + diff --git a/src/System.Diagnostics.CodeAnalysis.cs b/src/System.Diagnostics.CodeAnalysis.cs deleted file mode 100644 index 8a5b242094..0000000000 --- a/src/System.Diagnostics.CodeAnalysis.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#if !NET6_0_OR_GREATER - -#pragma warning disable CA1801, CA1822 - -namespace System.Diagnostics.CodeAnalysis -{ - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] - internal sealed class AllowNullAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] - internal sealed class DisallowNullAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - internal sealed class DoesNotReturnAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class DoesNotReturnIfAttribute : Attribute - { - public DoesNotReturnIfAttribute(bool parameterValue) { } - - public bool ParameterValue { get { throw null!; } } - } - - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Event | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] - internal sealed class ExcludeFromCodeCoverageAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] - internal sealed class MaybeNullAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class MaybeNullWhenAttribute : Attribute - { - public MaybeNullWhenAttribute(bool returnValue) { } - - public bool ReturnValue { get { throw null!; } } - } - - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] - internal sealed class NotNullAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] - internal sealed class NotNullIfNotNullAttribute : Attribute - { - public NotNullIfNotNullAttribute(string parameterName) { } - - public string ParameterName { get { throw null!; } } - } - - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class NotNullWhenAttribute : Attribute - { - public NotNullWhenAttribute(bool returnValue) { } - - public bool ReturnValue { get { throw null!; } } - } -} - -#endif \ No newline at end of file From 1d060beb1fe03122c8e72738b167e699bb4ac4da Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Tue, 20 Aug 2024 09:08:44 -0400 Subject: [PATCH 127/150] Response to PR review --- src/System.CommandLine.Subsystems/Pipeline.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 53bbb41760..38698015d0 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -28,7 +28,7 @@ public partial class Pipeline /// A help subsystem to replace the standard one. To add a subsystem, use /// A new pipeline. /// - /// The ValueProvider, ResponseSubystem, InvocationSubsystem, and ValidationSubsystem cannot be replaced. + /// The , , , and cannot be replaced. /// public static Pipeline Create(HelpSubsystem? help = null, VersionSubsystem? version = null, From 4e3de562616e7099ca4194b9c92b9e0f2e216a2b Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Fri, 16 Aug 2024 18:32:30 -0400 Subject: [PATCH 128/150] Replace some use of CliOption/CliArgument overloads with CliValueSymbol. --- .../ValueProvider.cs | 26 +++++++++---------- src/System.CommandLine/ParseResult.cs | 10 +++---- .../Parsing/CliSymbolResultInternal.cs | 4 +-- .../Parsing/SymbolLookupByName.cs | 2 +- .../Parsing/SymbolResultTree.cs | 6 ++--- 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/System.CommandLine.Subsystems/ValueProvider.cs b/src/System.CommandLine.Subsystems/ValueProvider.cs index 6741ff3400..fa447cb740 100644 --- a/src/System.CommandLine.Subsystems/ValueProvider.cs +++ b/src/System.CommandLine.Subsystems/ValueProvider.cs @@ -37,28 +37,26 @@ private bool TryGetValue(CliSymbol symbol, out T? value) public T? GetValue(CliValueSymbol valueSymbol) => GetValueInternal(valueSymbol); - private T? GetValueInternal(CliSymbol? symbol) + private T? GetValueInternal(CliValueSymbol? valueSymbol) { // NOTE: We use the subsystem's TryGetAnnotation here instead of the GetDefaultValue etc // extension methods, as the subsystem's TryGetAnnotation respects its annotation provider - return symbol switch + return valueSymbol switch { - not null when TryGetValue(symbol, out var value) + { } when TryGetValue(valueSymbol, out var value) => value, // It has already been retrieved at least once - CliArgument argument when parseResult?.GetValueResult(argument) is { } valueResult // GetValue not used because it would always return a value - => UseValue(symbol, valueResult.GetValue()), // Value was supplied during parsing, - CliOption option when parseResult?.GetValueResult(option) is { } valueResult // GetValue not used because it would always return a value - => UseValue(symbol, valueResult.GetValue()), // Value was supplied during parsing + { } when parseResult?.GetValueResult(valueSymbol) is { } valueResult // GetValue not used because it would always return a value + => UseValue(valueSymbol, valueResult.GetValue()), // Value was supplied during parsing, // Value was not supplied during parsing, determine default now // configuration values go here in precedence //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) // => UseValue(symbol, GetEnvByName(envName)), - not null when symbol.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation) - => UseValue(symbol, CalculatedDefault(symbol, (Func)defaultValueCalculation)), - not null when symbol.TryGetAnnotation(ValueAnnotations.DefaultValue, out T? explicitValue) - => UseValue(symbol, explicitValue), - null => throw new ArgumentNullException(nameof(symbol)), - _ => UseValue(symbol, default(T)) + { } when valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation) + => UseValue(valueSymbol, CalculatedDefault(valueSymbol, (Func)defaultValueCalculation)), + { } when valueSymbol.TryGetAnnotation( ValueAnnotations.DefaultValue, out T? explicitValue) + => UseValue(valueSymbol, explicitValue), + null => throw new ArgumentNullException(nameof(valueSymbol)), + _ => UseValue(valueSymbol, default(T)) }; TValue? UseValue(CliSymbol symbol, TValue? value) @@ -68,7 +66,7 @@ not null when symbol.TryGetAnnotation(ValueAnnotations.DefaultValue, out T? expl } } - private static T? CalculatedDefault(CliSymbol symbol, Func defaultValueCalculation) + private static T? CalculatedDefault(CliValueSymbol valueSymbol, Func defaultValueCalculation) { var objectValue = defaultValueCalculation(); var value = objectValue is null diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 2930f60d9b..07d068f747 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -172,8 +172,8 @@ CommandLineText is null public T? GetValue(CliOption option) => GetValueInternal(option); - private T? GetValueInternal(CliSymbol symbol) - => valueResultDictionary.TryGetValue(symbol, out var result) + private T? GetValueInternal(CliValueSymbol valueSymbol) + => valueResultDictionary.TryGetValue(valueSymbol, out var result) ? (T?)result.Value : default; @@ -203,15 +203,15 @@ CommandLineText is null */ /// - /// Gets the ValueResult, if any, for the specified option or argument + /// Gets the if any, for the specified option or argument /// /// The option or argument for which to find a result. /// A result for the specified option, or if it was not entered by the user. public CliValueResult? GetValueResult(CliValueSymbol valueSymbol) => GetValueResultInternal(valueSymbol); - private CliValueResult? GetValueResultInternal(CliSymbol symbol) - => valueResultDictionary.TryGetValue(symbol, out var result) + private CliValueResult? GetValueResultInternal(CliValueSymbol valueSymbol) + => valueResultDictionary.TryGetValue(valueSymbol, out var result) ? result : null; diff --git a/src/System.CommandLine/Parsing/CliSymbolResultInternal.cs b/src/System.CommandLine/Parsing/CliSymbolResultInternal.cs index 7c0b6c5fff..bf09249159 100644 --- a/src/System.CommandLine/Parsing/CliSymbolResultInternal.cs +++ b/src/System.CommandLine/Parsing/CliSymbolResultInternal.cs @@ -70,7 +70,7 @@ public IEnumerable Errors /// /// The argument for which to find a result. /// An argument result if the argument was matched by the parser or has a default value; otherwise, null. - internal CliArgumentResultInternal? GetResult(CliArgument argument) => SymbolResultTree.GetResult(argument); + internal CliArgumentResultInternal? GetResult(CliArgument argument) => SymbolResultTree.GetResultInternal(argument); /* Not used /// @@ -86,7 +86,7 @@ public IEnumerable Errors /// /// The option for which to find a result. /// An option result if the option was matched by the parser or has a default value; otherwise, null. - internal CliOptionResultInternal? GetResult(CliOption option) => SymbolResultTree.GetResult(option); + internal CliOptionResultInternal? GetResult(CliOption option) => SymbolResultTree.GetResultInternal(option); // TODO: directives /* diff --git a/src/System.CommandLine/Parsing/SymbolLookupByName.cs b/src/System.CommandLine/Parsing/SymbolLookupByName.cs index 414157c10d..d55b83a039 100644 --- a/src/System.CommandLine/Parsing/SymbolLookupByName.cs +++ b/src/System.CommandLine/Parsing/SymbolLookupByName.cs @@ -109,7 +109,7 @@ private bool TryGetSymbolAndParentInternal(string name, { if (commandCache.SymbolsByName.TryGetValue(name, out symbol)) { - if (symbol is not null && (!valuesOnly || (symbol is CliArgument or CliOption))) + if (symbol is not null && (!valuesOnly || (symbol is CliValueSymbol))) { parent = commandCache.Command; errorMessage = null; diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 651890925a..571403bc27 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -37,13 +37,13 @@ internal SymbolResultTree( internal int ErrorCount => Errors?.Count ?? 0; - internal CliArgumentResultInternal? GetResult(CliArgument argument) + internal CliArgumentResultInternal? GetResultInternal(CliArgument argument) => TryGetValue(argument, out CliSymbolResultInternal? result) ? (CliArgumentResultInternal)result : default; - internal CliCommandResultInternal? GetResult(CliCommand command) + internal CliCommandResultInternal? GetResultInternal(CliCommand command) => TryGetValue(command, out var result) ? (CliCommandResultInternal)result : default; - internal CliOptionResultInternal? GetResult(CliOption option) + internal CliOptionResultInternal? GetResultInternal(CliOption option) => TryGetValue(option, out CliSymbolResultInternal? result) ? (CliOptionResultInternal)result : default; // TODO: Determine how this is used. It appears to be O^n in the size of the tree and so if it is called multiple times, we should reconsider to avoid O^(N*M) From 6e74d6b4036fec83ef14c3337b78f63d572e4261 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Fri, 16 Aug 2024 18:32:30 -0400 Subject: [PATCH 129/150] Incorporate ValueProvider work --- src/System.CommandLine.Subsystems/ValueProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.CommandLine.Subsystems/ValueProvider.cs b/src/System.CommandLine.Subsystems/ValueProvider.cs index fa447cb740..af1984d5a3 100644 --- a/src/System.CommandLine.Subsystems/ValueProvider.cs +++ b/src/System.CommandLine.Subsystems/ValueProvider.cs @@ -53,7 +53,7 @@ private bool TryGetValue(CliSymbol symbol, out T? value) // => UseValue(symbol, GetEnvByName(envName)), { } when valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation) => UseValue(valueSymbol, CalculatedDefault(valueSymbol, (Func)defaultValueCalculation)), - { } when valueSymbol.TryGetAnnotation( ValueAnnotations.DefaultValue, out T? explicitValue) + { } when valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValue, out T? explicitValue) => UseValue(valueSymbol, explicitValue), null => throw new ArgumentNullException(nameof(valueSymbol)), _ => UseValue(valueSymbol, default(T)) From e9f3697bc8631573c6ba3e6c39f9ed9bd743f5cd Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 17 Aug 2024 09:36:45 -0400 Subject: [PATCH 130/150] Validation subsystem working with tests --- .../AlternateSubsystems.cs | 6 +- ...System.CommandLine.Subsystems.Tests.csproj | 1 + .../ValidationSubsystemTests.cs | 74 +++++ .../ValueSubsystemTests.cs | 1 - .../CompletionSubsystem.cs | 2 +- .../Directives/DiagramSubsystem.cs | 2 +- .../ErrorReportingSubsystem.cs | 2 +- .../HelpSubsystem.cs | 2 +- src/System.CommandLine.Subsystems/Pipeline.cs | 2 +- .../Annotations/ValidationAnnotations.cs | 13 - .../Annotations/ValueAnnotations.cs | 4 +- .../Annotations/ValueConditionAnnotations.cs | 18 ++ .../Subsystems/CliSubsystem.cs | 4 +- .../Validation/CommandValidator.cs | 22 ++ .../Validation/InclusiveGroupValidator.cs | 47 ++++ .../Validation/RangeValidator.cs | 39 +++ .../Validation/ValidationContext.cs | 18 ++ .../Validation/Validator.cs | 38 +++ .../Validation/ValueValidator.cs | 28 ++ .../ValidationSubsystem.cs | 257 +++++++++++++++++- .../ValueAnnotationExtensions.cs | 153 ++--------- .../ValueCondition.cs | 9 + .../ValueConditionAnnotationExtensions.cs | 42 +++ .../ValueConditions/InclusiveGroup.cs | 16 ++ .../ValueConditions/Range.cs | 23 ++ .../ValueConditions/RangeForAbsoluteValue.cs | 13 + .../VersionSubsystem.cs | 2 +- .../Parsing/CliValueResult.cs | 2 +- 28 files changed, 676 insertions(+), 164 deletions(-) create mode 100644 src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs delete mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/ValidationAnnotations.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs create mode 100644 src/System.CommandLine.Subsystems/Validation/CommandValidator.cs create mode 100644 src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs create mode 100644 src/System.CommandLine.Subsystems/Validation/RangeValidator.cs create mode 100644 src/System.CommandLine.Subsystems/Validation/ValidationContext.cs create mode 100644 src/System.CommandLine.Subsystems/Validation/Validator.cs create mode 100644 src/System.CommandLine.Subsystems/Validation/ValueValidator.cs create mode 100644 src/System.CommandLine.Subsystems/ValueCondition.cs create mode 100644 src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs create mode 100644 src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs create mode 100644 src/System.CommandLine.Subsystems/ValueConditions/Range.cs create mode 100644 src/System.CommandLine.Subsystems/ValueConditions/RangeForAbsoluteValue.cs diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index abab37351b..9745a61a90 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -10,7 +10,7 @@ internal class AlternateSubsystems { internal class AlternateVersion : VersionSubsystem { - protected override void Execute(PipelineResult pipelineResult) + public override void Execute(PipelineResult pipelineResult) { pipelineResult.ConsoleHack.WriteLine($"***{CliExecutable.ExecutableVersion}***"); pipelineResult.SetSuccess(); @@ -28,7 +28,7 @@ public VersionThatUsesHelpData(CliSymbol symbol) private CliSymbol Symbol { get; } - protected override void Execute(PipelineResult pipelineResult) + public override void Execute(PipelineResult pipelineResult) { TryGetAnnotation(Symbol, HelpAnnotations.Description, out string? description); pipelineResult.ConsoleHack.WriteLine(description); @@ -50,7 +50,7 @@ protected override void Initialize(InitializationContext context) InitializationWasRun = true; } - protected override void Execute(PipelineResult pipelineResult) + public override void Execute(PipelineResult pipelineResult) { ExecutionWasRun = true; base.Execute(pipelineResult); diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index 33a7efd6ef..2067a4a2ac 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -32,6 +32,7 @@ --> + diff --git a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs new file mode 100644 index 0000000000..789190d66f --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs @@ -0,0 +1,74 @@ +// 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 FluentAssertions; +using System.CommandLine.Directives; +using System.CommandLine.Parsing; +using Xunit; +using static System.CommandLine.Subsystems.Tests.TestData; + +namespace System.CommandLine.Subsystems.Tests; + +public class ValidationSubsystemTests +{ + // Running exactly the same code is important here because missing a step will result in a false positive. Ask me how I know + private (CliCommand rootCommand, CliConfiguration configuration) GetCliWithRange(T lowerBound, T upperBound) + where T: IComparable + { + var option = new CliOption("--intOpt"); + option.SetRange(lowerBound, upperBound); + var rootCommand = new CliRootCommand { option }; + return (rootCommand, new CliConfiguration(rootCommand)); + } + + private PipelineResult ExecutedPipelineResultForRange(T lowerBound, T upperBound, string input) + where T : IComparable + { + (var rootCommand, var configuration) = GetCliWithRange(lowerBound, upperBound); + var validationSubsystem = ValidationSubsystem.Create(); + var parseResult = CliParser.Parse(rootCommand, input, configuration); + var pipelineResult = new PipelineResult(parseResult, input, null); + validationSubsystem.Execute(pipelineResult); + return pipelineResult; + } + + [Fact] + public void Int_values_in_specified_range_do_not_have_errors() + { + var pipelineResult = ExecutedPipelineResultForRange(0, 50,"--intOpt 42"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().BeEmpty(); + } + + [Fact] + public void Int_values_not_in_specified_range_report_error() + { + var pipelineResult = ExecutedPipelineResultForRange(0, 5, "--intOpt 42"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().HaveCount(1); + var error = pipelineResult.GetErrors().First(); + // TODO: Create test mechanism for CliDiagnostics + } + + [Fact] + public void Int_values_on_lower_range_bound_do_not_report_error() + { + var pipelineResult = ExecutedPipelineResultForRange(42, 50, "--intOpt 42"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().BeEmpty(); + } + + [Fact] + public void Int_values_on_upper_range_bound_do_not_report_error() + { + var pipelineResult = ExecutedPipelineResultForRange(0, 42, "--intOpt 42"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().BeEmpty(); + } + + +} diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs index 40821cac5b..9fb7573a9d 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSubsystemTests.cs @@ -18,7 +18,6 @@ public void Values_that_are_entered_are_retrieved() var rootCommand = new CliRootCommand { option }; var configuration = new CliConfiguration(rootCommand); var pipeline = Pipeline.Create(); - var consoleHack = new ConsoleHack().RedirectToBuffer(true); var input = "--intOpt 42"; var parseResult = CliParser.Parse(rootCommand, input, configuration); diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index 04eea0ce16..8fd0156ba0 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -27,7 +27,7 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) ? false : false; - protected internal override void Execute(PipelineResult pipelineResult) + public override void Execute(PipelineResult pipelineResult) { pipelineResult.ConsoleHack.WriteLine("Not yet implemented"); pipelineResult.SetSuccess(); diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 796c7e7363..352d960e54 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -13,7 +13,7 @@ public class DiagramSubsystem(IAnnotationProvider? annotationProvider = null) //protected internal override bool GetIsActivated(ParseResult? parseResult) // => parseResult is not null && option is not null && parseResult.GetValue(option); - protected internal override void Execute(PipelineResult pipelineResult) + public override void Execute(PipelineResult pipelineResult) { // Gather locations //var locations = pipelineResult.ParseResult.LocationMap diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs index 9effb620f0..7c7432616b 100644 --- a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -23,7 +23,7 @@ protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.Errors.Any(); // TODO: properly test execute directly when parse result is usable in tests - protected internal override void Execute(PipelineResult pipelineResult) + public override void Execute(PipelineResult pipelineResult) { var _ = pipelineResult.ParseResult ?? throw new ArgumentException("The parse result has not been set", nameof(pipelineResult)); diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index 84626e8f21..ca4d99c2ae 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -38,7 +38,7 @@ protected internal override void Initialize(InitializationContext context) protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.GetValue(HelpOption); - protected internal override void Execute(PipelineResult pipelineResult) + public override void Execute(PipelineResult pipelineResult) { // TODO: Match testable output pattern pipelineResult.ConsoleHack.WriteLine("Help me!"); diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 38698015d0..03b7842166 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -58,7 +58,7 @@ private Pipeline() { Response = new ResponseSubsystem(); Invocation = new InvocationSubsystem(); - Validation = new ValidationSubsystem(); + Validation = ValidationSubsystem.Create(); // This order is based on: if the user entered both, which should they get? // * It is reasonable to diagram help and completion. More reasonable than getting help on Diagram or Completion diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValidationAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValidationAnnotations.cs deleted file mode 100644 index 78155e5358..0000000000 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValidationAnnotations.cs +++ /dev/null @@ -1,13 +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.Subsystems.Annotations; - -/// -/// IDs for well-known Version annotations. -/// -public static class ValidationAnnotations -{ - internal static string Prefix { get; } = nameof(SubsystemKind.Validation); - -} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs index 8212797714..ce95549067 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs @@ -14,7 +14,7 @@ public static class ValueAnnotations /// Default value for an option or argument /// /// - /// Must be actually the same type as the type parameter of + /// Should be the same type as the type parameter of /// the or . /// public static AnnotationId DefaultValue { get; } = new(Prefix, nameof(DefaultValue)); @@ -25,7 +25,7 @@ public static class ValueAnnotations /// /// Please use the extension methods and do not call this directly. /// - /// Must return a with the same type parameter as + /// Should use a with the same type parameter as /// the or . /// /// diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs new file mode 100644 index 0000000000..a24e424f62 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs @@ -0,0 +1,18 @@ +// 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.Subsystems.Annotations; + +/// +/// IDs for well-known Version annotations. +/// +public static class ValueConditionAnnotations +{ + // TODO: @mhutch What do you want the prefix to be for AnnotationIds that are not bound to a subsystem? + internal static string Prefix { get; } = ""; + + /// + /// Value conditions for a symbol + /// + public static AnnotationId ValueConditions { get; } = new(Prefix, nameof(ValueConditions)); +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 703716b5b6..d02f0e606a 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -108,10 +108,10 @@ protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId annotati /// /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate /// A PipelineResult object with information such as whether the CLI should terminate - protected internal virtual void Execute(PipelineResult pipelineResult) + public virtual void Execute(PipelineResult pipelineResult) => pipelineResult.NotRun(pipelineResult.ParseResult); - internal PipelineResult ExecuteIfNeeded(PipelineResult pipelineResult) + public PipelineResult ExecuteIfNeeded(PipelineResult pipelineResult) => ExecuteIfNeeded(pipelineResult.ParseResult, pipelineResult); internal PipelineResult ExecuteIfNeeded(ParseResult? parseResult, PipelineResult pipelineResult) diff --git a/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs b/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs new file mode 100644 index 0000000000..86c5ec0b0b --- /dev/null +++ b/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs @@ -0,0 +1,22 @@ +// 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.Parsing; + +namespace System.CommandLine.Validation; + +public abstract class CommandValidator : Validator +{ + protected CommandValidator(string name, Type valueConditionType, params Type[] moreValueConditionTypes) + : base(name, valueConditionType, moreValueConditionTypes) + { } + + // These methods provide consistent messages + protected TValueCondition GetTypedValueConditionOrThrow(ValueCondition valueCondition) + where TValueCondition : ValueCondition + => valueCondition is TValueCondition typedValueCondition + ? typedValueCondition + : throw new ArgumentException($"{Name} validation failed to find bounds"); + + public abstract void Validate(CliCommandResult commandResult, ValueCondition valueCondition, ValidationContext validationContext); +} diff --git a/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs b/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs new file mode 100644 index 0000000000..a8d9cb6bcf --- /dev/null +++ b/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs @@ -0,0 +1,47 @@ +// 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.Parsing; +using System.Text; + +namespace System.CommandLine.Validation; + +public class InclusiveGroupValidator : CommandValidator +{ + public InclusiveGroupValidator() : base(nameof(InclusiveGroup), typeof(InclusiveGroup)) + { } + + public override void Validate(CliCommandResult commandResult, + ValueCondition valueCondition, ValidationContext validationContext) + { + var commandSymbol = commandResult.Command; + // TODO: Write the SymbolsInUse method. I think this should allow for default values, so it requires some thought. Hopefully ValueResult already returns only those vaues that the user entered. + var symbolsInUse = commandResult.ValueResults.Select(x => x.ValueSymbol); // commandResult.SymbolsInUse(); + var inclusiveGroup = GetTypedValueConditionOrThrow(valueCondition); + var groupMembers = inclusiveGroup.Members; + var groupInUse = groupMembers + .Any(x => symbolsInUse.Contains(x)); + if (!groupInUse) + { + return; + } + // TODO: Lazily create the missing member list + // TODO: See if there is a LINQ set method for "all not in the other list" + var missingMembers = new List(); + foreach (var member in groupMembers) + { + if (!symbolsInUse.Contains(member)) + { + missingMembers.Add(member); + } + } + if (missingMembers is not null && missingMembers.Any()) + { + var pluralToBe = "are"; + var singularToBe = "is"; + validationContext.PipelineResult.AddError(new ParseError( $"The members {string.Join(", ", groupMembers.Select(m => m.Name))} " + + $"must all be used if one is used. {string.Join(", ", missingMembers.Select(m => m.Name))} " + + $"{(missingMembers.Skip(1).Any() ? pluralToBe : singularToBe)} missing.")); + } + } +} diff --git a/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs b/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs new file mode 100644 index 0000000000..423f92ecea --- /dev/null +++ b/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs @@ -0,0 +1,39 @@ +// 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.Parsing; + +namespace System.CommandLine.Validation; + +public class RangeValidator : ValueValidator +{ + public RangeValidator() : base(nameof(Range), typeof(Range)) + { } + + public override void Validate(object? value, CliValueSymbol valueSymbol, + CliValueResult? valueResult, ValueCondition valueCondition, ValidationContext validationContext) + { + + var range = GetTypedValueConditionOrThrow(valueCondition); + var comparableValue = GetValueAsTypeOrThrow(value); + + // TODO: Replace the strings we are comparing with a diagnostic ID when we update ParseError + if (range.LowerBound is not null) + { + if (comparableValue.CompareTo(range.LowerBound) < 0) + { + validationContext.PipelineResult.AddError(new ParseError( $"The value for '{valueSymbol.Name}' is below the lower bound of {range.LowerBound}")); + } + } + + if (range.UpperBound is not null) + { + if (comparableValue.CompareTo(range.UpperBound) > 0) + { + validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {range.LowerBound}")); + } + } + } + + +} diff --git a/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs b/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs new file mode 100644 index 0000000000..bd444e7552 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs @@ -0,0 +1,18 @@ +// 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.Validation; + +public class ValidationContext +{ + public ValidationContext(PipelineResult pipelineResult, ValidationSubsystem validationSubsystem) + { + PipelineResult = pipelineResult; + ValidationSubsystem = validationSubsystem; + } + + public PipelineResult PipelineResult { get; } + public Pipeline Pipeline => PipelineResult.Pipeline; + public ValidationSubsystem ValidationSubsystem { get; } + public ParseResult? ParseResult => PipelineResult.ParseResult; +} diff --git a/src/System.CommandLine.Subsystems/Validation/Validator.cs b/src/System.CommandLine.Subsystems/Validation/Validator.cs new file mode 100644 index 0000000000..e0fab3d17f --- /dev/null +++ b/src/System.CommandLine.Subsystems/Validation/Validator.cs @@ -0,0 +1,38 @@ +using System.CommandLine.Parsing; +// 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.Validation; + +public abstract class Validator +{ + public Validator(string name, Type valueConditionType, params Type[] moreValueConditionTypes) + { + Name = name; + ValueConditionTypes = [valueConditionType, .. moreValueConditionTypes]; + } + + public string Name { get; } + + public Type[] ValueConditionTypes { get; } + + /// + /// Adds a validation CliDiagnostic that will alter be added to the PipelineResult. Not yet implemented to support that + /// + /// + /// + /// + /// + /// + /// This method needs to be evolved as we replace ParseError with CliError + /// + protected static List AddValidationError(ref List? parseErrors, string message, IEnumerable errorValues) + { + // TODO: Review the handling of errors. They are currently transient and returned by the Validate method, and to avoid allocating in the case of no errors (the common case) this method is used. This adds complexity to creating a new validator. + parseErrors ??= new List(); + parseErrors.Add(new ParseError(message)); + return parseErrors; + } + + +} \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs b/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs new file mode 100644 index 0000000000..f7738cdee9 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs @@ -0,0 +1,28 @@ +// 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.Parsing; + +namespace System.CommandLine.Validation; + +public abstract class ValueValidator : Validator +{ + protected ValueValidator(string name, Type valueConditionType, params Type[] moreValueConditionTypes) + : base(name, valueConditionType, moreValueConditionTypes) + { } + + // These methods provide consistent messages + protected TDataValueCondition GetTypedValueConditionOrThrow(ValueCondition valueCondition) + where TDataValueCondition : ValueCondition + => valueCondition is TDataValueCondition typedValueCondition + ? typedValueCondition + : throw new ArgumentException($"{Name} validation failed to find bounds"); + + protected TValue GetValueAsTypeOrThrow(object? value) + => value is TValue typedValue + ? typedValue + : throw new InvalidOperationException($"{Name} validation does not apply to this type"); + + public abstract void Validate(object? value, CliValueSymbol valueSymbol, + CliValueResult? valueResult, ValueCondition valueCondition, ValidationContext validationContext); +} diff --git a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs index 29a8a5c5cd..cd8bb9d52c 100644 --- a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs @@ -1,11 +1,260 @@ // 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; +using System.CommandLine.Parsing; using System.CommandLine.Subsystems; -using System.CommandLine.Subsystems.Annotations; +using System.CommandLine.Validation; namespace System.CommandLine; -public class ValidationSubsystem(IAnnotationProvider? annotationProvider = null) - : CliSubsystem(ValidationAnnotations.Prefix, SubsystemKind.Validation, annotationProvider) -{ } +// TODO: Add support for terminating validator. This is needed at least for required because it would be annoying to get an error that you forgot to enter something, and also all the validation errors for the default value Probably other uses, so generalize to termintating. +public sealed class ValidationSubsystem : CliSubsystem +{ + // The type here is the ValueCondition type + private Dictionary validators = []; + + private ValidationSubsystem(IAnnotationProvider? annotationProvider = null) + : base("", SubsystemKind.Validation, annotationProvider) + { } + + public static ValidationSubsystem Create() + { + var newValidationSubsystem = new ValidationSubsystem(); + newValidationSubsystem.AddValidator(new RangeValidator()); + newValidationSubsystem.AddValidator(new InclusiveGroupValidator()); + return newValidationSubsystem; + } + + public static ValidationSubsystem CreateEmpty() + => new ValidationSubsystem(); + + public Validator this[Type type] + { + get { return validators[type]; } + } + + public void AddValidator(Validator validator) + { + foreach (var type in validator.ValueConditionTypes) + { + validators[type] = validator; + } + } + + protected internal override bool GetIsActivated(ParseResult? parseResult) => true; + + public override void Execute(PipelineResult pipelineResult) + { + if (pipelineResult.ParseResult is null) + { + return; + } + var validationContext = new ValidationContext(pipelineResult, this); + var commandResults = CommandAndAncestors(pipelineResult.ParseResult.CommandResult); + var valueSymbols = GetValueSymbols(commandResults); + foreach (var symbol in valueSymbols) + { + ValidateValue(symbol, validationContext); + } + foreach (var commandResult in commandResults) + { + ValidateCommand(commandResult, validationContext); + } + } + + private void ValidateValue(CliValueSymbol valueSymbol, ValidationContext validationContext) + { + var valueConditions = valueSymbol.GetValueConditions(); + if (valueConditions is null) + { + return; // nothing to do + } + + var value = validationContext.PipelineResult.GetValue(valueSymbol); + var valueResult = validationContext.ParseResult?.GetValueResult(valueSymbol); + foreach (var condition in valueConditions) + { + ValidateValueCondition(value, valueSymbol, valueResult, condition, validationContext); + } + } + + private void ValidateCommand(CliCommandResult commandResult, ValidationContext validationContext) + { + var valueConditions = commandResult.Command.GetValueConditions(); + if (valueConditions is null) + { + return; // nothing to do + } + + foreach (var condition in valueConditions) + { + ValidateCommandCondition(commandResult, condition, validationContext); + } + } + + private static List GetValueSymbols(IEnumerable commandResults) + => commandResults + .SelectMany(commandResult => commandResult.ValueResults.Select(valueResult => valueResult.ValueSymbol)) + .Distinct() + .ToList(); + + // Consider moving to CliCommandResult + private static IEnumerable CommandAndAncestors(CliCommandResult commandResult) + => commandResult.Parent is not null + ? [commandResult, .. global::System.CommandLine.ValidationSubsystem.CommandAndAncestors(commandResult.Parent)] + : [commandResult]; + + private void ValidateValueCondition(object? value, CliValueSymbol valueSymbol, CliValueResult? valueResult, ValueCondition condition, ValidationContext validationContext) + { + Validator? validator = GetValidator(condition); + switch (validator) + { + case null: + break; + case ValueValidator valueValidator: + valueValidator.Validate(value, valueSymbol, valueResult, condition, validationContext); + break; + default: + throw new InvalidOperationException("Validator must be derive from ValueValidator"); + } + } + + private Validator? GetValidator(ValueCondition condition) + { + if (!validators.TryGetValue(condition.GetType(), out var validator) || validator is null) + { + if (condition.MustHaveValidator) + { + // Output missing validator error + } + } + + return validator; + } + + private void ValidateCommandCondition(CliCommandResult commandResult, ValueCondition condition, ValidationContext validationContext) + { + Validator? validator = GetValidator(condition); + switch (validator) + { + case null: + break; + case CommandValidator commandValidator: + commandValidator.Validate(commandResult, condition, validationContext); + break; + default: + throw new InvalidOperationException("Validator must be derive from CommandValidator"); + } + } + + + + /* if (pipelineResult.ParseResult is null) + { + // Nothing to do, validation is called prior to parsing. Is this an exception or error? + return; + } + var validationContext = new ValidationContext(pipelineResult, this); + var errors = new List(); + if (pipelineResult.ParseResult is null) + { + return; // nothing to do + } + CliCommandResult commandResult = pipelineResult.ParseResult.CommandResult; + var commandResults = GetResultAndParents(commandResult); + // Not sure whether to do commands or values first + ValidateCommands(commandResults, errors, commandValidators, validationContext); + ValidateValues(commandResults, errors, dataValidators, validationContext); + pipelineResult.AddErrors(errors); + + // TODO: Consider which of these local methods to make protected and possibly overridable + static void ValidateValues(IEnumerable commandResults, List errors, + Dictionary validators, ValidationContext validationContext) + { + var dataSymbols = GetDataSymbols(commandResults); + foreach (var dataSymbol in dataSymbols) + { + ValidateValue(dataSymbol, errors, validators, validationContext); + } + } + + static void ValidateValue(CliValueSymbol dataSymbol, List errors, Dictionary validators, ValidationContext validationContext) + { + // TODO: If this remains local, this test may not be needed + if (validationContext.ParseResult is null) + { + // Nothing to do, validation is called prior to parsing. Any error should be reported elsewhere + return; + } + var valueConditions = dataSymbol.GetValueConditions(); + if (valueConditions is null) + { + return; // This is a common case, and nothing to do + } + var value = validationContext.PipelineResult.GetValue(dataSymbol); + var valueResult = validationContext.ParseResult.GetValueResult(dataSymbol); + foreach (var valueCondition in valueConditions) + { + if (!validators.TryGetValue(valueCondition.GetType(), out var validator)) + { + // TODO: This seems an issue - an exception or an error that a validator is missing + continue; + } + var newErrors = validator.Validate(value, valueResult, valueCondition, validationContext); + if (newErrors is not null) + { + errors.AddRange(newErrors); + } + } + } + + static IEnumerable GetDataSymbols(IEnumerable commandResults) + => commandResults + .SelectMany(cr => cr.ValueResults + .Select(c => c.ValueSymbol)) + .Distinct() + .ToList(); + + static IEnumerable GetResultAndParents(CliCommandResult commandResult) + { + var list = new List(); + var current = commandResult; + while (current is not null) + { + list.Add(current); + current = current.Parent; + } + return list; + } + + static void ValidateCommands(IEnumerable commandValueResults, List errors, + Dictionary validators, ValidationContext validationContext) + { + // Walk up the results tree. Not needed for ValueResults because they are collapsed + foreach (var commandValueResult in commandValueResults) + { + var symbol = commandValueResult.Command; + var valueConditions = symbol.GetCommandValueConditions(); + if (valueConditions is null) + { + return; + } + foreach (var valueCondition in valueConditions) + { + if (!validators.TryGetValue(valueCondition.GetType(), out var validator)) + { + // TODO: This seems an issue - an exception or an error that a validator is missing + continue; + } + var newErrors = validator.Validate(commandValueResult, valueCondition, validationContext); + if (newErrors is not null) + { + errors.AddRange(newErrors); + } + } + } + } + } + */ +} diff --git a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs index 7506e45a5f..d7b936a837 100644 --- a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs @@ -8,30 +8,7 @@ namespace System.CommandLine; public static class ValueAnnotationExtensions { - /// - /// Sets the default value annotation on the - /// - /// The type of the option value - /// The option - /// The default value for the option - /// The , to enable fluent construction of symbols with annotations. - public static CliOption WithDefaultValue (this CliOption option, TValue defaultValue) - { - option.SetDefaultValue(defaultValue); - return option; - } - - /// - /// Sets the default value annotation on the - /// - /// The type of the option value - /// The option - /// The default value for the option - public static void SetDefaultValue(this CliOption option, TValue defaultValue) - { - option.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); - } - + /// /// Get the default value annotation for the /// @@ -43,27 +20,17 @@ public static void SetDefaultValue(this CliOption option, TValue /// which calculates the actual default value, based on the default value annotation and default value calculation, /// whether directly stored on the symbol or from the subsystem's . /// - public static TValue? GetDefaultValueAnnotation(this CliOption option) - { - if (option.TryGetAnnotation(ValueAnnotations.DefaultValue, out TValue? defaultValue)) - { - return defaultValue; - } - return default; - } + public static bool TryGetDefaultValueAnnotation(this CliValueSymbol valueSymbol, out TValue? defaultValue) + => valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValue, out defaultValue); /// - /// Sets the default value annotation on the + /// Sets the default value annotation on the /// - /// The type of the argument value - /// The argument - /// The default value for the argument - /// The , to enable fluent construction of symbols with annotations. - public static CliArgument WithDefaultValue(this CliArgument argument, TValue defaultValue) - { - argument.SetDefaultValue(defaultValue); - return argument; - } + /// The type of the option value + /// The option + /// The default value for the option + public static void SetDefaultValue(this CliOption option, TValue defaultValue) + => option.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); /// /// Sets the default value annotation on the @@ -72,54 +39,8 @@ public static CliArgument WithDefaultValue(this CliArgumentThe argument /// The default value for the argument /// The , to enable fluent construction of symbols with annotations. - public static void SetDefaultValue(this CliArgument argument, TValue defaultValue) - { - argument.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); - } - - /// - /// Get the default value annotation for the - /// - /// The type of the argument value - /// The argument - /// The argument's default value annotation if any, otherwise - /// - /// This is intended to be called by CLI authors. Subsystems should instead call , - /// which calculates the actual default value, based on the default value annotation and default value calculation, - /// whether directly stored on the symbol or from the subsystem's . - /// - public static TValue? GetDefaultValueAnnotation(this CliArgument argument) - { - if (argument.TryGetAnnotation(ValueAnnotations.DefaultValue, out TValue? defaultValue)) - { - return (TValue?)defaultValue; - } - return default; - } - - /// - /// Sets the default value calculation for the - /// - /// The type of the option value - /// The option - /// The default value calculation for the option - /// The , to enable fluent construction of symbols with annotations. - public static CliOption WithDefaultValueCalculation(this CliOption option, Func defaultValueCalculation) - { - option.SetDefaultValueCalculation(defaultValueCalculation); - return option; - } - - /// - /// Sets the default value calculation for the - /// - /// The type of the option value - /// The option - /// The default value calculation for the option - public static void SetDefaultValueCalculation(this CliOption option, Func defaultValueCalculation) - { - option.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); - } + public static void SetDefaultValue(this CliArgument argument, TValue defaultValue) + => argument.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); /// /// Get the default value calculation for the @@ -132,27 +53,17 @@ public static void SetDefaultValueCalculation(this CliOption opt /// which calculates the actual default value, based on the default value annotation and default value calculation, /// whether directly stored on the symbol or from the subsystem's . /// - public static Func? GetDefaultValueCalculation(this CliOption option) - { - if (option.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation)) - { - return defaultValueCalculation; - } - return default; - } + public static bool TryGetDefaultValueCalculation(this CliValueSymbol valueSymbol, out Func? calculation) + => valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out calculation); /// - /// Sets the default value calculation for the + /// Sets the default value calculation for the /// - /// The type of the argument value - /// The argument - /// The default value calculation for the argument - /// The , to enable fluent construction of symbols with annotations. - public static CliArgument WithDefaultValueCalculation(this CliArgument argument, Func defaultValueCalculation) - { - argument.SetDefaultValueCalculation(defaultValueCalculation); - return argument; - } + /// The type of the option value + /// The option + /// The default value calculation for the option + public static void SetDefaultValueCalculation(this CliOption option, Func defaultValueCalculation) + => option.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); /// /// Sets the default value calculation for the @@ -161,28 +72,6 @@ public static CliArgument WithDefaultValueCalculation(this CliAr /// The argument /// The default value calculation for the argument /// The , to enable fluent construction of symbols with annotations. - public static void SetDefaultValueCalculation(this CliArgument argument, Func defaultValueCalculation) - { - argument.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); - } - - /// - /// Get the default value calculation for the - /// - /// The type of the argument value - /// The argument - /// The argument's default value calculation if any, otherwise - /// - /// This is intended to be called by CLI authors. Subsystems should instead call , - /// which calculates the actual default value, based on the default value annotation and default value calculation, - /// whether directly stored on the symbol or from the subsystem's . - /// - public static Func? GetDefaultValueCalculation(this CliArgument argument) - { - if (argument.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation)) - { - return defaultValueCalculation; - } - return default; - } + public static void SetDefaultValueCalculation(this CliArgument argument, Func defaultValueCalculation) + => argument.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); } diff --git a/src/System.CommandLine.Subsystems/ValueCondition.cs b/src/System.CommandLine.Subsystems/ValueCondition.cs new file mode 100644 index 0000000000..35f8c81f13 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueCondition.cs @@ -0,0 +1,9 @@ +// 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; + +public abstract class ValueCondition(bool mustHaveValidator = true) +{ + public bool MustHaveValidator { get; } = mustHaveValidator; +} diff --git a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs new file mode 100644 index 0000000000..9ecdd98615 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs @@ -0,0 +1,42 @@ +using System.CommandLine; +using System.CommandLine.Subsystems.Annotations; + +namespace System.CommandLine +{ + public static class ValueConditionAnnotationExtensions + { + public static void SetRange(this CliValueSymbol symbol, T lowerBound, T upperBound) + where T : IComparable + { + var range = new Range + { + ValueType = symbol.ValueType, + LowerBound = lowerBound, + UpperBound = upperBound + }; + + symbol.SetValueCondition(range); + } + + public static void SetInclusiveGroup(this CliCommand symbol, IEnumerable group) + => symbol.SetValueCondition(new InclusiveGroup(group)); + + public static void SetValueCondition(this TSymbol symbol, TValueCondition valueCondition) + where TSymbol : CliSymbol + where TValueCondition : ValueCondition + { + if (!symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions)) + { + valueConditions = []; + symbol.SetAnnotation(ValueConditionAnnotations.ValueConditions, valueConditions); + } + valueConditions.Add(valueCondition); + } + + public static List? GetValueConditions(this CliSymbol symbol) + => symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) + ? valueConditions + : null; + + } +} diff --git a/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs b/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs new file mode 100644 index 0000000000..4d9ccdedb1 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs @@ -0,0 +1,16 @@ +// 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; + +public class InclusiveGroup : ValueCondition +{ + private IEnumerable group = []; + + public InclusiveGroup(IEnumerable group) + { + this.group = group; + } + + public IEnumerable Members => group.ToList(); +} diff --git a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs new file mode 100644 index 0000000000..f273689ef9 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs @@ -0,0 +1,23 @@ +// 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; + +public class Range : ValueCondition +{ + public static Range CreateRange(T? lowerBound, T? upperBound) + where T : IComparable + { + return new Range + { + LowerBound = lowerBound, + UpperBound = upperBound, + ValueType = typeof(T) + }; + } + + public required Type ValueType { get; init; } + + public object? LowerBound { get; init; } + public object? UpperBound { get; init; } +} diff --git a/src/System.CommandLine.Subsystems/ValueConditions/RangeForAbsoluteValue.cs b/src/System.CommandLine.Subsystems/ValueConditions/RangeForAbsoluteValue.cs new file mode 100644 index 0000000000..06e433bec9 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueConditions/RangeForAbsoluteValue.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace System.CommandLine.ValueConditions +{ + public class RangeForAbsoluteValue : Range + { + + } +} diff --git a/src/System.CommandLine.Subsystems/VersionSubsystem.cs b/src/System.CommandLine.Subsystems/VersionSubsystem.cs index 7da5a655e4..1aac8b75a1 100644 --- a/src/System.CommandLine.Subsystems/VersionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/VersionSubsystem.cs @@ -47,7 +47,7 @@ protected internal override void Initialize(InitializationContext context) protected internal override bool GetIsActivated(ParseResult? parseResult) => parseResult is not null && parseResult.GetValue("--version"); - protected internal override void Execute(PipelineResult pipelineResult) + public override void Execute(PipelineResult pipelineResult) { var subsystemVersion = SpecificVersion; var version = subsystemVersion is null diff --git a/src/System.CommandLine/Parsing/CliValueResult.cs b/src/System.CommandLine/Parsing/CliValueResult.cs index 1bcce4e7b2..7c2b524bed 100644 --- a/src/System.CommandLine/Parsing/CliValueResult.cs +++ b/src/System.CommandLine/Parsing/CliValueResult.cs @@ -29,7 +29,7 @@ internal CliValueResult( /// /// The CliSymbol the value is for. This is always a CliOption or CliArgument. /// - public CliSymbol ValueSymbol { get; } + public CliValueSymbol ValueSymbol { get; } internal object? Value { get; } From ce67d0fd16054015c2bf652200f28979f723fd0e Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Wed, 21 Aug 2024 15:36:50 -0400 Subject: [PATCH 131/150] Most of the changes from PR Review --- .../CommandCondition.cs | 10 + .../Annotations/ValueConditionAnnotations.cs | 3 +- .../Validation/CommandValidator.cs | 9 +- .../Validation/ICommandValidator.cs | 13 ++ .../Validation/IValueValidator.cs | 12 ++ .../Validation/InclusiveGroupValidator.cs | 4 +- .../Validation/RangeValidator.cs | 25 +-- .../Validation/Validator.cs | 16 +- .../Validation/ValueValidator.cs | 7 - .../ValidationSubsystem.cs | 172 +++++------------- .../ValueCondition.cs | 5 +- .../ValueConditionAnnotationExtensions.cs | 82 ++++++--- .../ValueConditions/InclusiveGroup.cs | 5 +- .../ValueConditions/Range.cs | 57 ++++-- .../ValueConditions/RangeBound.cs | 10 + .../ValueConditions/RangeForAbsoluteValue.cs | 13 -- .../ValueConditions/ValueSource.cs | 60 ++++++ 17 files changed, 278 insertions(+), 225 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/CommandCondition.cs create mode 100644 src/System.CommandLine.Subsystems/Validation/ICommandValidator.cs create mode 100644 src/System.CommandLine.Subsystems/Validation/IValueValidator.cs create mode 100644 src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs delete mode 100644 src/System.CommandLine.Subsystems/ValueConditions/RangeForAbsoluteValue.cs create mode 100644 src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs diff --git a/src/System.CommandLine.Subsystems/CommandCondition.cs b/src/System.CommandLine.Subsystems/CommandCondition.cs new file mode 100644 index 0000000000..6c6cf72009 --- /dev/null +++ b/src/System.CommandLine.Subsystems/CommandCondition.cs @@ -0,0 +1,10 @@ +// 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; + +public abstract class CommandCondition(string name) +{ + public virtual bool MustHaveValidator { get; } = true; + public string Name { get; } = name; +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs index a24e424f62..d89efc2b8b 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs @@ -8,8 +8,7 @@ namespace System.CommandLine.Subsystems.Annotations; /// public static class ValueConditionAnnotations { - // TODO: @mhutch What do you want the prefix to be for AnnotationIds that are not bound to a subsystem? - internal static string Prefix { get; } = ""; + internal static string Prefix { get; } = "General"; /// /// Value conditions for a symbol diff --git a/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs b/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs index 86c5ec0b0b..c2aadd25e9 100644 --- a/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs @@ -11,12 +11,5 @@ protected CommandValidator(string name, Type valueConditionType, params Type[] m : base(name, valueConditionType, moreValueConditionTypes) { } - // These methods provide consistent messages - protected TValueCondition GetTypedValueConditionOrThrow(ValueCondition valueCondition) - where TValueCondition : ValueCondition - => valueCondition is TValueCondition typedValueCondition - ? typedValueCondition - : throw new ArgumentException($"{Name} validation failed to find bounds"); - - public abstract void Validate(CliCommandResult commandResult, ValueCondition valueCondition, ValidationContext validationContext); + public abstract void Validate(CliCommandResult commandResult, CommandCondition commandCondition, ValidationContext validationContext); } diff --git a/src/System.CommandLine.Subsystems/Validation/ICommandValidator.cs b/src/System.CommandLine.Subsystems/Validation/ICommandValidator.cs new file mode 100644 index 0000000000..2286fde581 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Validation/ICommandValidator.cs @@ -0,0 +1,13 @@ +// 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.Parsing; + +namespace System.CommandLine.Validation; + +public interface ICommandValidator +{ + + void Validate(CliCommandResult commandResult, CommandCondition commandCondition, ValidationContext validationContext); +} + diff --git a/src/System.CommandLine.Subsystems/Validation/IValueValidator.cs b/src/System.CommandLine.Subsystems/Validation/IValueValidator.cs new file mode 100644 index 0000000000..2747fb78c1 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Validation/IValueValidator.cs @@ -0,0 +1,12 @@ +// 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.Parsing; + +namespace System.CommandLine.Validation; + +public interface IValueValidator +{ + void Validate(object? value, CliValueSymbol valueSymbol, + CliValueResult? valueResult, ValueCondition valueCondition, ValidationContext validationContext); +} diff --git a/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs b/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs index a8d9cb6bcf..c478be5a98 100644 --- a/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Parsing; -using System.Text; +using System.CommandLine.ValueConditions; namespace System.CommandLine.Validation; @@ -12,7 +12,7 @@ public InclusiveGroupValidator() : base(nameof(InclusiveGroup), typeof(Inclusive { } public override void Validate(CliCommandResult commandResult, - ValueCondition valueCondition, ValidationContext validationContext) + CommandCondition valueCondition, ValidationContext validationContext) { var commandSymbol = commandResult.Command; // TODO: Write the SymbolsInUse method. I think this should allow for default values, so it requires some thought. Hopefully ValueResult already returns only those vaues that the user entered. diff --git a/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs b/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs index 423f92ecea..10c0fa0293 100644 --- a/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs @@ -5,33 +5,22 @@ namespace System.CommandLine.Validation; -public class RangeValidator : ValueValidator +public class RangeValidator : ValueValidator, IValueValidator { - public RangeValidator() : base(nameof(Range), typeof(Range)) + public RangeValidator() : base(nameof(ValueConditions.Range), typeof(ValueConditions.Range)) { } public override void Validate(object? value, CliValueSymbol valueSymbol, CliValueResult? valueResult, ValueCondition valueCondition, ValidationContext validationContext) { - - var range = GetTypedValueConditionOrThrow(valueCondition); - var comparableValue = GetValueAsTypeOrThrow(value); - - // TODO: Replace the strings we are comparing with a diagnostic ID when we update ParseError - if (range.LowerBound is not null) + if (valueCondition is IValueValidator valueValidator) { - if (comparableValue.CompareTo(range.LowerBound) < 0) - { - validationContext.PipelineResult.AddError(new ParseError( $"The value for '{valueSymbol.Name}' is below the lower bound of {range.LowerBound}")); - } + valueValidator.Validate(value, valueSymbol, valueResult, valueCondition, validationContext); + return; } - - if (range.UpperBound is not null) + if (valueCondition.MustHaveValidator) { - if (comparableValue.CompareTo(range.UpperBound) > 0) - { - validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {range.LowerBound}")); - } + validationContext.PipelineResult.AddError(new ParseError($"Range validator missing for {valueResult.ValueSymbol.Name}")); } } diff --git a/src/System.CommandLine.Subsystems/Validation/Validator.cs b/src/System.CommandLine.Subsystems/Validation/Validator.cs index e0fab3d17f..11eb556e6f 100644 --- a/src/System.CommandLine.Subsystems/Validation/Validator.cs +++ b/src/System.CommandLine.Subsystems/Validation/Validator.cs @@ -34,5 +34,19 @@ protected static List AddValidationError(ref List? parse return parseErrors; } + // These methods provide consistent messages + protected TCommandCondition GetTypedValueConditionOrThrow(CommandCondition commandCondition) + where TCommandCondition : CommandCondition + => commandCondition is TCommandCondition typedValueCondition + ? typedValueCondition + : throw new ArgumentException($"{Name} validation failed to validator"); -} \ No newline at end of file + // These methods provide consistent messages + protected TDataValueCondition GetTypedValueConditionOrThrow(ValueCondition valueCondition) + where TDataValueCondition : ValueCondition + => valueCondition is TDataValueCondition typedValueCondition + ? typedValueCondition + : throw new ArgumentException($"{Name} validation failed to find bounds"); + + +} diff --git a/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs b/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs index f7738cdee9..7029dd37a3 100644 --- a/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs @@ -11,13 +11,6 @@ protected ValueValidator(string name, Type valueConditionType, params Type[] mor : base(name, valueConditionType, moreValueConditionTypes) { } - // These methods provide consistent messages - protected TDataValueCondition GetTypedValueConditionOrThrow(ValueCondition valueCondition) - where TDataValueCondition : ValueCondition - => valueCondition is TDataValueCondition typedValueCondition - ? typedValueCondition - : throw new ArgumentException($"{Name} validation failed to find bounds"); - protected TValue GetValueAsTypeOrThrow(object? value) => value is TValue typedValue ? typedValue diff --git a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs index cd8bb9d52c..7b77a6ec70 100644 --- a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs @@ -12,7 +12,8 @@ namespace System.CommandLine; public sealed class ValidationSubsystem : CliSubsystem { // The type here is the ValueCondition type - private Dictionary validators = []; + private Dictionary valueValidators = []; + private Dictionary commandValidators = []; private ValidationSubsystem(IAnnotationProvider? annotationProvider = null) : base("", SubsystemKind.Validation, annotationProvider) @@ -29,16 +30,19 @@ public static ValidationSubsystem Create() public static ValidationSubsystem CreateEmpty() => new ValidationSubsystem(); - public Validator this[Type type] + public void AddValidator(ValueValidator validator) { - get { return validators[type]; } + foreach (var type in validator.ValueConditionTypes) + { + valueValidators[type] = validator; + } } - public void AddValidator(Validator validator) + public void AddValidator(CommandValidator validator) { foreach (var type in validator.ValueConditionTypes) { - validators[type] = validator; + commandValidators[type] = validator; } } @@ -81,7 +85,7 @@ private void ValidateValue(CliValueSymbol valueSymbol, ValidationContext validat private void ValidateCommand(CliCommandResult commandResult, ValidationContext validationContext) { - var valueConditions = commandResult.Command.GetValueConditions(); + var valueConditions = commandResult.Command.GetCommandConditions(); if (valueConditions is null) { return; // nothing to do @@ -102,159 +106,71 @@ private static List GetValueSymbols(IEnumerable CommandAndAncestors(CliCommandResult commandResult) => commandResult.Parent is not null - ? [commandResult, .. global::System.CommandLine.ValidationSubsystem.CommandAndAncestors(commandResult.Parent)] + ? [commandResult, .. CommandAndAncestors(commandResult.Parent)] : [commandResult]; private void ValidateValueCondition(object? value, CliValueSymbol valueSymbol, CliValueResult? valueResult, ValueCondition condition, ValidationContext validationContext) { - Validator? validator = GetValidator(condition); - switch (validator) + if (condition is IValueValidator conditionValidator) { - case null: - break; - case ValueValidator valueValidator: - valueValidator.Validate(value, valueSymbol, valueResult, condition, validationContext); - break; - default: - throw new InvalidOperationException("Validator must be derive from ValueValidator"); + conditionValidator.Validate(value, valueSymbol, valueResult, condition, validationContext); + return; } - } - - private Validator? GetValidator(ValueCondition condition) - { - if (!validators.TryGetValue(condition.GetType(), out var validator) || validator is null) + ValueValidator? validator = GetValidator(condition); + if (validator == null) { if (condition.MustHaveValidator) { - // Output missing validator error + validationContext.PipelineResult.AddError(new ParseError($"{valueSymbol.Name} must have {condition.Name} validator.")); } + return; } + validator.Validate(value, valueSymbol, valueResult, condition, validationContext); - return validator; } - private void ValidateCommandCondition(CliCommandResult commandResult, ValueCondition condition, ValidationContext validationContext) + private ValueValidator? GetValidator(ValueCondition condition) { - Validator? validator = GetValidator(condition); - switch (validator) + if (!valueValidators.TryGetValue(condition.GetType(), out var validator) || validator is null) { - case null: - break; - case CommandValidator commandValidator: - commandValidator.Validate(commandResult, condition, validationContext); - break; - default: - throw new InvalidOperationException("Validator must be derive from CommandValidator"); - } - } - - - - /* if (pipelineResult.ParseResult is null) - { - // Nothing to do, validation is called prior to parsing. Is this an exception or error? - return; - } - var validationContext = new ValidationContext(pipelineResult, this); - var errors = new List(); - if (pipelineResult.ParseResult is null) - { - return; // nothing to do - } - CliCommandResult commandResult = pipelineResult.ParseResult.CommandResult; - var commandResults = GetResultAndParents(commandResult); - // Not sure whether to do commands or values first - ValidateCommands(commandResults, errors, commandValidators, validationContext); - ValidateValues(commandResults, errors, dataValidators, validationContext); - pipelineResult.AddErrors(errors); - - // TODO: Consider which of these local methods to make protected and possibly overridable - static void ValidateValues(IEnumerable commandResults, List errors, - Dictionary validators, ValidationContext validationContext) - { - var dataSymbols = GetDataSymbols(commandResults); - foreach (var dataSymbol in dataSymbols) + if (condition.MustHaveValidator) { - ValidateValue(dataSymbol, errors, validators, validationContext); + // Output missing validator error } } - static void ValidateValue(CliValueSymbol dataSymbol, List errors, Dictionary validators, ValidationContext validationContext) + return validator; + } + + private CommandValidator? GetValidator(CommandCondition condition) + { + if (!commandValidators.TryGetValue(condition.GetType(), out var validator) || validator is null) { - // TODO: If this remains local, this test may not be needed - if (validationContext.ParseResult is null) - { - // Nothing to do, validation is called prior to parsing. Any error should be reported elsewhere - return; - } - var valueConditions = dataSymbol.GetValueConditions(); - if (valueConditions is null) - { - return; // This is a common case, and nothing to do - } - var value = validationContext.PipelineResult.GetValue(dataSymbol); - var valueResult = validationContext.ParseResult.GetValueResult(dataSymbol); - foreach (var valueCondition in valueConditions) + if (condition.MustHaveValidator) { - if (!validators.TryGetValue(valueCondition.GetType(), out var validator)) - { - // TODO: This seems an issue - an exception or an error that a validator is missing - continue; - } - var newErrors = validator.Validate(value, valueResult, valueCondition, validationContext); - if (newErrors is not null) - { - errors.AddRange(newErrors); - } + // Output missing validator error } } - static IEnumerable GetDataSymbols(IEnumerable commandResults) - => commandResults - .SelectMany(cr => cr.ValueResults - .Select(c => c.ValueSymbol)) - .Distinct() - .ToList(); + return validator; + } - static IEnumerable GetResultAndParents(CliCommandResult commandResult) + private void ValidateCommandCondition(CliCommandResult commandResult, CommandCondition condition, ValidationContext validationContext) + { + if (condition is ICommandValidator conditionValidator) { - var list = new List(); - var current = commandResult; - while (current is not null) - { - list.Add(current); - current = current.Parent; - } - return list; + conditionValidator.Validate(commandResult, condition, validationContext); + return; } - - static void ValidateCommands(IEnumerable commandValueResults, List errors, - Dictionary validators, ValidationContext validationContext) + CommandValidator? validator = GetValidator(condition); + if (validator == null) { - // Walk up the results tree. Not needed for ValueResults because they are collapsed - foreach (var commandValueResult in commandValueResults) + if (condition.MustHaveValidator) { - var symbol = commandValueResult.Command; - var valueConditions = symbol.GetCommandValueConditions(); - if (valueConditions is null) - { - return; - } - foreach (var valueCondition in valueConditions) - { - if (!validators.TryGetValue(valueCondition.GetType(), out var validator)) - { - // TODO: This seems an issue - an exception or an error that a validator is missing - continue; - } - var newErrors = validator.Validate(commandValueResult, valueCondition, validationContext); - if (newErrors is not null) - { - errors.AddRange(newErrors); - } - } + validationContext.PipelineResult.AddError(new ParseError($"{commandResult.Command.Name} must have {condition.Name} validator.")); } + return; } + validator.Validate(commandResult, condition, validationContext); } - */ } diff --git a/src/System.CommandLine.Subsystems/ValueCondition.cs b/src/System.CommandLine.Subsystems/ValueCondition.cs index 35f8c81f13..9c20f7f45e 100644 --- a/src/System.CommandLine.Subsystems/ValueCondition.cs +++ b/src/System.CommandLine.Subsystems/ValueCondition.cs @@ -3,7 +3,8 @@ namespace System.CommandLine; -public abstract class ValueCondition(bool mustHaveValidator = true) +public abstract class ValueCondition(string name) { - public bool MustHaveValidator { get; } = mustHaveValidator; + public virtual bool MustHaveValidator { get; } = true; + public string Name { get; } = name; } diff --git a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs index 9ecdd98615..3a3720afd1 100644 --- a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs @@ -1,42 +1,66 @@ using System.CommandLine; using System.CommandLine.Subsystems.Annotations; +using System.CommandLine.ValueConditions; -namespace System.CommandLine +namespace System.CommandLine; + +public static class ValueConditionAnnotationExtensions { - public static class ValueConditionAnnotationExtensions + public static void SetRange(this CliValueSymbol symbol, T lowerBound, T upperBound) + where T : IComparable + { + var range = new Range(lowerBound, upperBound); + + symbol.SetValueCondition(range); + } + + public static void SetInclusiveGroup(this CliCommand symbol, IEnumerable group) + => symbol.SetValueCondition(new InclusiveGroup(group)); + + public static void SetValueCondition(this TValueSymbol symbol, TValueCondition valueCondition) + where TValueSymbol : CliValueSymbol + where TValueCondition : ValueCondition { - public static void SetRange(this CliValueSymbol symbol, T lowerBound, T upperBound) - where T : IComparable + if (!symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions)) { - var range = new Range - { - ValueType = symbol.ValueType, - LowerBound = lowerBound, - UpperBound = upperBound - }; - - symbol.SetValueCondition(range); + valueConditions = []; + symbol.SetAnnotation(ValueConditionAnnotations.ValueConditions, valueConditions); } + valueConditions.Add(valueCondition); + } - public static void SetInclusiveGroup(this CliCommand symbol, IEnumerable group) - => symbol.SetValueCondition(new InclusiveGroup(group)); - - public static void SetValueCondition(this TSymbol symbol, TValueCondition valueCondition) - where TSymbol : CliSymbol - where TValueCondition : ValueCondition + public static void SetValueCondition(this CliCommand symbol, TValueCondition valueCondition) + where TValueCondition : CommandCondition + { + if (!symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions)) { - if (!symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions)) - { - valueConditions = []; - symbol.SetAnnotation(ValueConditionAnnotations.ValueConditions, valueConditions); - } - valueConditions.Add(valueCondition); + valueConditions = []; + symbol.SetAnnotation(ValueConditionAnnotations.ValueConditions, valueConditions); } + valueConditions.Add(valueCondition); + } + + public static List? GetValueConditions(this CliValueSymbol symbol) + => symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) + ? valueConditions + : null; + + public static List? GetCommandConditions(this CliCommand symbol) + => symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) + ? valueConditions + : null; + + public static TCondition? GetValueCondition(this CliValueSymbol symbol) + where TCondition : ValueCondition + => !symbol.TryGetAnnotation(ValueConditionAnnotations.ValueConditions, out List? valueConditions) + ? null + : valueConditions.OfType().LastOrDefault(); + + public static TCondition? GetCommandCondition(this CliCommand symbol) + where TCondition : CommandCondition + => !symbol.TryGetAnnotation(ValueConditionAnnotations.ValueConditions, out List? valueConditions) + ? null + : valueConditions.OfType().LastOrDefault(); - public static List? GetValueConditions(this CliSymbol symbol) - => symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) - ? valueConditions - : null; - } } diff --git a/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs b/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs index 4d9ccdedb1..f16c0e851c 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs @@ -1,13 +1,14 @@ // 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; +namespace System.CommandLine.ValueConditions; -public class InclusiveGroup : ValueCondition +public class InclusiveGroup : CommandCondition { private IEnumerable group = []; public InclusiveGroup(IEnumerable group) + : base(nameof(InclusiveGroup)) { this.group = group; } diff --git a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs index f273689ef9..15797b0c94 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs @@ -1,23 +1,54 @@ // 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; +using System.CommandLine.Parsing; +using System.CommandLine.Validation; -public class Range : ValueCondition +namespace System.CommandLine.ValueConditions; + +public abstract class Range : ValueCondition { - public static Range CreateRange(T? lowerBound, T? upperBound) - where T : IComparable + protected Range(Type valueType) + : base(nameof(Range)) { - return new Range - { - LowerBound = lowerBound, - UpperBound = upperBound, - ValueType = typeof(T) - }; + ValueType = valueType; } + public Type ValueType { get; } +} + +public class Range(T? lowerBound, T? upperBound) + : Range(typeof(T)), IValueValidator + where T : IComparable +{ + public void Validate(object? value, + CliValueSymbol valueSymbol, + CliValueResult? valueResult, + ValueCondition valueCondition, + ValidationContext validationContext) + { + if (valueCondition != this) throw new InvalidOperationException("Unexpected value condition type"); + if (value is not T comparableValue) throw new InvalidOperationException("Unexpected value type"); - public required Type ValueType { get; init; } + if (comparableValue is null) return; // nothing to do + + // TODO: Replace the strings we are comparing with a diagnostic ID when we update ParseError + if (LowerBound is not null) + { + if (comparableValue.CompareTo(LowerBound) < 0) + { + validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is below the lower bound of {LowerBound}")); + } + } + + if (UpperBound is not null) + { + if (comparableValue.CompareTo(UpperBound) > 0) + { + validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {UpperBound}")); + } + } + } - public object? LowerBound { get; init; } - public object? UpperBound { get; init; } + public T? LowerBound { get; init; } = lowerBound; + public T? UpperBound { get; init; } = upperBound; } diff --git a/src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs b/src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs new file mode 100644 index 0000000000..ed6cbc36c2 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs @@ -0,0 +1,10 @@ +// 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.ValueConditions; + +public class RangeBound +{ + public ValueSource ValueSource { get; } + public bool Exclusive { get; } +} diff --git a/src/System.CommandLine.Subsystems/ValueConditions/RangeForAbsoluteValue.cs b/src/System.CommandLine.Subsystems/ValueConditions/RangeForAbsoluteValue.cs deleted file mode 100644 index 06e433bec9..0000000000 --- a/src/System.CommandLine.Subsystems/ValueConditions/RangeForAbsoluteValue.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace System.CommandLine.ValueConditions -{ - public class RangeForAbsoluteValue : Range - { - - } -} diff --git a/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs new file mode 100644 index 0000000000..f43cc74489 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs @@ -0,0 +1,60 @@ +// 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.ValueConditions; + +public abstract class ValueSource +{ + public abstract object? GetValue(PipelineResult pipelineResult); + + // TODO: Should we use ToString() here? + public abstract string Description { get; } +} + +public abstract class ValueSource : ValueSource +{ + public abstract T GetTypedValue(PipelineResult pipelineResult); + + public override object? GetValue(PipelineResult pipelineResult) + { + return GetTypedValue(pipelineResult); + } +} + +public class SimpleValueSource(T value, string description) + : ValueSource +{ + public override string Description { get; } = description; + + public override T GetTypedValue(PipelineResult pipelineResult) + => value; +} + +// Find an example of when this is useful beyond Random and Guid. Is a time lag between building the CLI and validating important (DateTime.Now()) +public class CalculatedValueSource(Func calculation, string description) + : ValueSource +{ + public override string Description { get; } = description; + + public override T GetTypedValue(PipelineResult pipelineResult) + => calculation(); +} + +public class RelativeToSymbolValueSource(CliValueSymbol otherSymbol, Func calculation, string description) + : ValueSource +{ + public override string Description { get; } = description; + + public override T GetTypedValue(PipelineResult pipelineResult) + => calculation(pipelineResult.GetValue(otherSymbol)); +} + +public class RelativeToEnvironmentVariableValueSource(string environmentVariableName, Func calculation, string description) + : ValueSource +{ + public override string Description { get; } = description; + + public override T GetTypedValue(PipelineResult pipelineResult) + => calculation(Environment.GetEnvironmentVariable(environmentVariableName)); +} + From 5ba9513a3ff81262b31ae8a68dd09b67bdfdcae6 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Wed, 21 Aug 2024 17:11:29 -0400 Subject: [PATCH 132/150] Incorporated RangeBound and ValueSource --- .../ValidationSubsystemTests.cs | 188 ++++++++++++++++-- .../ValueConditionAnnotationExtensions.cs | 26 ++- .../ValueConditions/Range.cs | 12 +- .../ValueConditions/RangeBound.cs | 22 +- .../ValueConditions/ValueSource.cs | 14 +- 5 files changed, 236 insertions(+), 26 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs index 789190d66f..74d5c7a422 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using FluentAssertions; +using Microsoft.VisualBasic.FileIO; using System.CommandLine.Directives; using System.CommandLine.Parsing; +using System.CommandLine.ValueConditions; using Xunit; using static System.CommandLine.Subsystems.Tests.TestData; @@ -12,22 +14,33 @@ namespace System.CommandLine.Subsystems.Tests; public class ValidationSubsystemTests { // Running exactly the same code is important here because missing a step will result in a false positive. Ask me how I know - private (CliCommand rootCommand, CliConfiguration configuration) GetCliWithRange(T lowerBound, T upperBound) - where T: IComparable + private CliOption GetOptionWithSimpleRange(T lowerBound, T upperBound) + where T : IComparable { var option = new CliOption("--intOpt"); option.SetRange(lowerBound, upperBound); - var rootCommand = new CliRootCommand { option }; - return (rootCommand, new CliConfiguration(rootCommand)); + return option; } - private PipelineResult ExecutedPipelineResultForRange(T lowerBound, T upperBound, string input) + private CliOption GetOptionWithRangeBounds(RangeBound lowerBound, RangeBound upperBound) where T : IComparable { - (var rootCommand, var configuration) = GetCliWithRange(lowerBound, upperBound); + var option = new CliOption("--intOpt"); + option.SetRange(lowerBound, upperBound); + return option; + } + + private PipelineResult ExecutedPipelineResultForRangeOption(CliOption option, string input) + { + var command = new CliRootCommand { option }; + return ExecutedPipelineResultForCommand(command, input); + } + + private PipelineResult ExecutedPipelineResultForCommand(CliCommand command, string input) + { var validationSubsystem = ValidationSubsystem.Create(); - var parseResult = CliParser.Parse(rootCommand, input, configuration); - var pipelineResult = new PipelineResult(parseResult, input, null); + var parseResult = CliParser.Parse(command, input, new CliConfiguration(command)); + var pipelineResult = new PipelineResult(parseResult, input, Pipeline.CreateEmpty()); validationSubsystem.Execute(pipelineResult); return pipelineResult; } @@ -35,16 +48,33 @@ private PipelineResult ExecutedPipelineResultForRange(T lowerBound, T upperBo [Fact] public void Int_values_in_specified_range_do_not_have_errors() { - var pipelineResult = ExecutedPipelineResultForRange(0, 50,"--intOpt 42"); + var option = GetOptionWithSimpleRange(0, 50); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); pipelineResult.Should().NotBeNull(); pipelineResult.GetErrors().Should().BeEmpty(); } [Fact] - public void Int_values_not_in_specified_range_report_error() + public void Int_values_above_upper_bound_report_error() + { + var option = GetOptionWithSimpleRange(0, 5); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().HaveCount(1); + var error = pipelineResult.GetErrors().First(); + // TODO: Create test mechanism for CliDiagnostics + } + + [Fact] + public void Int_below_lower_bound_report_error() { - var pipelineResult = ExecutedPipelineResultForRange(0, 5, "--intOpt 42"); + var option = GetOptionWithSimpleRange(0, 5); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt -42"); pipelineResult.Should().NotBeNull(); pipelineResult.GetErrors().Should().HaveCount(1); @@ -55,7 +85,9 @@ public void Int_values_not_in_specified_range_report_error() [Fact] public void Int_values_on_lower_range_bound_do_not_report_error() { - var pipelineResult = ExecutedPipelineResultForRange(42, 50, "--intOpt 42"); + var option = GetOptionWithSimpleRange(42, 50); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); pipelineResult.Should().NotBeNull(); pipelineResult.GetErrors().Should().BeEmpty(); @@ -64,11 +96,141 @@ public void Int_values_on_lower_range_bound_do_not_report_error() [Fact] public void Int_values_on_upper_range_bound_do_not_report_error() { - var pipelineResult = ExecutedPipelineResultForRange(0, 42, "--intOpt 42"); + var option = GetOptionWithSimpleRange(0, 42); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); pipelineResult.Should().NotBeNull(); pipelineResult.GetErrors().Should().BeEmpty(); } + [Fact] + public void Values_below_calculated_lower_bound_report_error() + { + var option = GetOptionWithRangeBounds(RangeBound.Create(() => 1), 50); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 0"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().HaveCount(1); + var error = pipelineResult.GetErrors().First(); + // TODO: Create test mechanism for CliDiagnostics + } + + + [Fact] + public void Values_within_calculated_range_do_not_report_error() + { + var option = GetOptionWithRangeBounds(RangeBound.Create(() => 1), RangeBound.Create(() => 50)); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().BeEmpty(); + } + + [Fact] + public void Values_above_calculated_upper_bound_report_error() + { + var option = GetOptionWithRangeBounds(0,RangeBound.Create(() => 40)); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().HaveCount(1); + var error = pipelineResult.GetErrors().First(); + // TODO: Create test mechanism for CliDiagnostics + } + + [Fact] + public void Values_below_relative_lower_bound_report_error() + { + var otherOption = new CliOption("-a"); + var option = GetOptionWithRangeBounds(RangeBound.Create(otherOption, o => (int)o + 1), 50); + var command = new CliCommand("cmd") { option, otherOption }; + + var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 0 -a 0"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().HaveCount(1); + var error = pipelineResult.GetErrors().First(); + // TODO: Create test mechanism for CliDiagnostics + } + + + [Fact] + public void Values_within_relative_range_do_not_report_error() + { + var otherOption = new CliOption("-a"); + var option = GetOptionWithRangeBounds(RangeBound.Create(otherOption, o => (int)o + 1), RangeBound.Create(otherOption, o => (int)o + 10)); + var command = new CliCommand("cmd") { option, otherOption }; + + var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 11 -a 3"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().BeEmpty(); + } + + [Fact] + public void Values_above_relative_upper_bound_report_error() + { + var otherOption = new CliOption("-a"); + var option = GetOptionWithRangeBounds(0, RangeBound.Create(otherOption, o => (int)o + 10)); + var command = new CliCommand("cmd") { option, otherOption }; + + var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 9 -a -2"); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().HaveCount(1); + var error = pipelineResult.GetErrors().First(); + // TODO: Create test mechanism for CliDiagnostics + } + + [Fact] + public void Values_below_environment_lower_bound_report_error() + { + var envName = "SYSTEM_COMMANDLINE_LOWERBOUND"; + Environment.SetEnvironmentVariable(envName, "2"); + var option = GetOptionWithRangeBounds(RangeBound.Create(envName, s => int.Parse(s) + 1), 50); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 2"); + Environment.SetEnvironmentVariable(envName, null); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().HaveCount(1); + var error = pipelineResult.GetErrors().First(); + // TODO: Create test mechanism for CliDiagnostics + } + + + [Fact] + public void Values_within_environment_range_do_not_report_error() + { + var envName = "SYSTEM_COMMANDLINE_LOWERBOUND"; + Environment.SetEnvironmentVariable(envName, "2"); + var option = GetOptionWithRangeBounds(RangeBound.Create(envName, s => int.Parse(s) + 1), 50); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 11"); + Environment.SetEnvironmentVariable(envName, null); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().BeEmpty(); + } + + [Fact] + public void Values_above_environment_upper_bound_report_error() + { + var envName = "SYSTEM_COMMANDLINE_LOWERBOUND"; + Environment.SetEnvironmentVariable(envName, "2"); + var option = GetOptionWithRangeBounds(0,RangeBound.Create(envName, s => int.Parse(s) + 1)); + + var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 4"); + Environment.SetEnvironmentVariable(envName, null); + + pipelineResult.Should().NotBeNull(); + pipelineResult.GetErrors().Should().HaveCount(1); + var error = pipelineResult.GetErrors().First(); + // TODO: Create test mechanism for CliDiagnostics + } } diff --git a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs index 3a3720afd1..bd8f8f8cb5 100644 --- a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs @@ -14,6 +14,30 @@ public static void SetRange(this CliValueSymbol symbol, T lowerBound, T upper symbol.SetValueCondition(range); } + public static void SetRange(this CliValueSymbol symbol, RangeBound lowerBound, T upperBound) + where T : IComparable + { + var range = new Range(lowerBound, upperBound); + + symbol.SetValueCondition(range); + } + + public static void SetRange(this CliValueSymbol symbol, T lowerBound, RangeBound upperBound) + where T : IComparable + { + var range = new Range(lowerBound, upperBound); + + symbol.SetValueCondition(range); + } + + public static void SetRange(this CliValueSymbol symbol, RangeBound lowerBound, RangeBound upperBound) + where T : IComparable + { + var range = new Range(lowerBound, upperBound); + + symbol.SetValueCondition(range); + } + public static void SetInclusiveGroup(this CliCommand symbol, IEnumerable group) => symbol.SetValueCondition(new InclusiveGroup(group)); @@ -51,7 +75,7 @@ public static void SetValueCondition(this CliCommand symbol, TV : null; public static TCondition? GetValueCondition(this CliValueSymbol symbol) - where TCondition : ValueCondition + where TCondition : ValueCondition => !symbol.TryGetAnnotation(ValueConditionAnnotations.ValueConditions, out List? valueConditions) ? null : valueConditions.OfType().LastOrDefault(); diff --git a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs index 15797b0c94..743a41d579 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs @@ -16,7 +16,7 @@ protected Range(Type valueType) public Type ValueType { get; } } -public class Range(T? lowerBound, T? upperBound) +public class Range(RangeBound? lowerBound, RangeBound? upperBound) : Range(typeof(T)), IValueValidator where T : IComparable { @@ -34,7 +34,8 @@ public void Validate(object? value, // TODO: Replace the strings we are comparing with a diagnostic ID when we update ParseError if (LowerBound is not null) { - if (comparableValue.CompareTo(LowerBound) < 0) + var lowerValue = LowerBound.ValueSource.GetTypedValue(validationContext.PipelineResult); + if (comparableValue.CompareTo(lowerValue) < 0) { validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is below the lower bound of {LowerBound}")); } @@ -42,13 +43,14 @@ public void Validate(object? value, if (UpperBound is not null) { - if (comparableValue.CompareTo(UpperBound) > 0) + var upperValue = UpperBound.ValueSource.GetTypedValue(validationContext.PipelineResult); + if (comparableValue.CompareTo(upperValue) > 0) { validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {UpperBound}")); } } } - public T? LowerBound { get; init; } = lowerBound; - public T? UpperBound { get; init; } = upperBound; + public RangeBound? LowerBound { get; init; } = lowerBound; + public RangeBound? UpperBound { get; init; } = upperBound; } diff --git a/src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs b/src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs index ed6cbc36c2..fd37277a35 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs @@ -3,8 +3,24 @@ namespace System.CommandLine.ValueConditions; -public class RangeBound +public class RangeBound(ValueSource valueSource, bool exclusive = false) { - public ValueSource ValueSource { get; } - public bool Exclusive { get; } + + public static implicit operator RangeBound(T value) => RangeBound.Create(value); + public static implicit operator RangeBound(Func calculated) => RangeBound.Create(calculated); + + public static RangeBound Create(T value, string? description = null) + => new(new SimpleValueSource(value, description)); + + public static RangeBound Create(Func calculation, string? description = null) + => new(new CalculatedValueSource(calculation)); + + public static RangeBound Create(CliValueSymbol otherSymbol, Func calculation, string? description = null) + => new(new RelativeToSymbolValueSource(otherSymbol, calculation, description)); + + public static RangeBound Create(string environmentVariableName, Func calculation, string? description = null) + => new(new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description)); + + public ValueSource ValueSource { get; } = valueSource; + public bool Exclusive { get; } = exclusive; } diff --git a/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs index f43cc74489..81cbb9ee3c 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs @@ -1,6 +1,8 @@ // 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 static System.Runtime.InteropServices.JavaScript.JSType; + namespace System.CommandLine.ValueConditions; public abstract class ValueSource @@ -19,9 +21,13 @@ public abstract class ValueSource : ValueSource { return GetTypedValue(pipelineResult); } + + public static implicit operator ValueSource(T value) => new SimpleValueSource(value); + public static implicit operator ValueSource(Func calculated) => new CalculatedValueSource(calculated); + } -public class SimpleValueSource(T value, string description) +public class SimpleValueSource(T value, string? description = null) : ValueSource { public override string Description { get; } = description; @@ -31,7 +37,7 @@ public override T GetTypedValue(PipelineResult pipelineResult) } // Find an example of when this is useful beyond Random and Guid. Is a time lag between building the CLI and validating important (DateTime.Now()) -public class CalculatedValueSource(Func calculation, string description) +public class CalculatedValueSource(Func calculation, string? description = null) : ValueSource { public override string Description { get; } = description; @@ -40,7 +46,7 @@ public override T GetTypedValue(PipelineResult pipelineResult) => calculation(); } -public class RelativeToSymbolValueSource(CliValueSymbol otherSymbol, Func calculation, string description) +public class RelativeToSymbolValueSource(CliValueSymbol otherSymbol, Func calculation, string? description) : ValueSource { public override string Description { get; } = description; @@ -49,7 +55,7 @@ public override T GetTypedValue(PipelineResult pipelineResult) => calculation(pipelineResult.GetValue(otherSymbol)); } -public class RelativeToEnvironmentVariableValueSource(string environmentVariableName, Func calculation, string description) +public class RelativeToEnvironmentVariableValueSource(string environmentVariableName, Func calculation, string? description) : ValueSource { public override string Description { get; } = description; From fae097a479fbefb739a5e5811c93a46316b25546 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Thu, 22 Aug 2024 09:34:20 -0400 Subject: [PATCH 133/150] Removed RangeBound, added enum RangeBounds to Range This removes a layer by directly using ValueSource as it becomes a first class concept --- .../ValidationSubsystemTests.cs | 20 +++++++------- .../ValueConditionAnnotationExtensions.cs | 6 ++--- .../ValueConditions/Range.cs | 12 +++++---- .../ValueConditions/RangeBound.cs | 26 ------------------- .../ValueConditions/RangeBounds.cs | 13 ++++++++++ .../ValueConditions/ValueSource.cs | 11 ++++++++ 6 files changed, 44 insertions(+), 44 deletions(-) delete mode 100644 src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs create mode 100644 src/System.CommandLine.Subsystems/ValueConditions/RangeBounds.cs diff --git a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs index 74d5c7a422..e985b04554 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs @@ -22,7 +22,7 @@ private CliOption GetOptionWithSimpleRange(T lowerBound, T upperBound) return option; } - private CliOption GetOptionWithRangeBounds(RangeBound lowerBound, RangeBound upperBound) + private CliOption GetOptionWithRangeBounds(ValueSource lowerBound, ValueSource upperBound) where T : IComparable { var option = new CliOption("--intOpt"); @@ -107,7 +107,7 @@ public void Int_values_on_upper_range_bound_do_not_report_error() [Fact] public void Values_below_calculated_lower_bound_report_error() { - var option = GetOptionWithRangeBounds(RangeBound.Create(() => 1), 50); + var option = GetOptionWithRangeBounds(ValueSource.Create(() => 1), 50); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 0"); @@ -121,7 +121,7 @@ public void Values_below_calculated_lower_bound_report_error() [Fact] public void Values_within_calculated_range_do_not_report_error() { - var option = GetOptionWithRangeBounds(RangeBound.Create(() => 1), RangeBound.Create(() => 50)); + var option = GetOptionWithRangeBounds(ValueSource.Create(() => 1), ValueSource.Create(() => 50)); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); @@ -132,7 +132,7 @@ public void Values_within_calculated_range_do_not_report_error() [Fact] public void Values_above_calculated_upper_bound_report_error() { - var option = GetOptionWithRangeBounds(0,RangeBound.Create(() => 40)); + var option = GetOptionWithRangeBounds(0, ValueSource.Create(() => 40)); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); @@ -146,7 +146,7 @@ public void Values_above_calculated_upper_bound_report_error() public void Values_below_relative_lower_bound_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds(RangeBound.Create(otherOption, o => (int)o + 1), 50); + var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (int)o + 1), 50); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 0 -a 0"); @@ -162,7 +162,7 @@ public void Values_below_relative_lower_bound_report_error() public void Values_within_relative_range_do_not_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds(RangeBound.Create(otherOption, o => (int)o + 1), RangeBound.Create(otherOption, o => (int)o + 10)); + var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (int)o + 1), ValueSource.Create(otherOption, o => (int)o + 10)); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 11 -a 3"); @@ -175,7 +175,7 @@ public void Values_within_relative_range_do_not_report_error() public void Values_above_relative_upper_bound_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds(0, RangeBound.Create(otherOption, o => (int)o + 10)); + var option = GetOptionWithRangeBounds(0, ValueSource.Create(otherOption, o => (int)o + 10)); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 9 -a -2"); @@ -191,7 +191,7 @@ public void Values_below_environment_lower_bound_report_error() { var envName = "SYSTEM_COMMANDLINE_LOWERBOUND"; Environment.SetEnvironmentVariable(envName, "2"); - var option = GetOptionWithRangeBounds(RangeBound.Create(envName, s => int.Parse(s) + 1), 50); + var option = GetOptionWithRangeBounds(ValueSource.Create(envName, s => int.Parse(s) + 1), 50); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 2"); Environment.SetEnvironmentVariable(envName, null); @@ -208,7 +208,7 @@ public void Values_within_environment_range_do_not_report_error() { var envName = "SYSTEM_COMMANDLINE_LOWERBOUND"; Environment.SetEnvironmentVariable(envName, "2"); - var option = GetOptionWithRangeBounds(RangeBound.Create(envName, s => int.Parse(s) + 1), 50); + var option = GetOptionWithRangeBounds(ValueSource.Create(envName, s => int.Parse(s) + 1), 50); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 11"); Environment.SetEnvironmentVariable(envName, null); @@ -222,7 +222,7 @@ public void Values_above_environment_upper_bound_report_error() { var envName = "SYSTEM_COMMANDLINE_LOWERBOUND"; Environment.SetEnvironmentVariable(envName, "2"); - var option = GetOptionWithRangeBounds(0,RangeBound.Create(envName, s => int.Parse(s) + 1)); + var option = GetOptionWithRangeBounds(0, ValueSource.Create(envName, s => int.Parse(s) + 1)); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 4"); Environment.SetEnvironmentVariable(envName, null); diff --git a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs index bd8f8f8cb5..7082f449bc 100644 --- a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs @@ -14,7 +14,7 @@ public static void SetRange(this CliValueSymbol symbol, T lowerBound, T upper symbol.SetValueCondition(range); } - public static void SetRange(this CliValueSymbol symbol, RangeBound lowerBound, T upperBound) + public static void SetRange(this CliValueSymbol symbol, ValueSource lowerBound, T upperBound) where T : IComparable { var range = new Range(lowerBound, upperBound); @@ -22,7 +22,7 @@ public static void SetRange(this CliValueSymbol symbol, RangeBound lowerBo symbol.SetValueCondition(range); } - public static void SetRange(this CliValueSymbol symbol, T lowerBound, RangeBound upperBound) + public static void SetRange(this CliValueSymbol symbol, T lowerBound, ValueSource upperBound) where T : IComparable { var range = new Range(lowerBound, upperBound); @@ -30,7 +30,7 @@ public static void SetRange(this CliValueSymbol symbol, T lowerBound, RangeBo symbol.SetValueCondition(range); } - public static void SetRange(this CliValueSymbol symbol, RangeBound lowerBound, RangeBound upperBound) + public static void SetRange(this CliValueSymbol symbol, ValueSource lowerBound, ValueSource upperBound) where T : IComparable { var range = new Range(lowerBound, upperBound); diff --git a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs index 743a41d579..b511fa2d32 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs @@ -16,7 +16,7 @@ protected Range(Type valueType) public Type ValueType { get; } } -public class Range(RangeBound? lowerBound, RangeBound? upperBound) +public class Range(ValueSource? lowerBound, ValueSource? upperBound, RangeBounds rangeBound = 0) : Range(typeof(T)), IValueValidator where T : IComparable { @@ -34,7 +34,7 @@ public void Validate(object? value, // TODO: Replace the strings we are comparing with a diagnostic ID when we update ParseError if (LowerBound is not null) { - var lowerValue = LowerBound.ValueSource.GetTypedValue(validationContext.PipelineResult); + var lowerValue = LowerBound.GetTypedValue(validationContext.PipelineResult); if (comparableValue.CompareTo(lowerValue) < 0) { validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is below the lower bound of {LowerBound}")); @@ -43,7 +43,7 @@ public void Validate(object? value, if (UpperBound is not null) { - var upperValue = UpperBound.ValueSource.GetTypedValue(validationContext.PipelineResult); + var upperValue = UpperBound.GetTypedValue(validationContext.PipelineResult); if (comparableValue.CompareTo(upperValue) > 0) { validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {UpperBound}")); @@ -51,6 +51,8 @@ public void Validate(object? value, } } - public RangeBound? LowerBound { get; init; } = lowerBound; - public RangeBound? UpperBound { get; init; } = upperBound; + public ValueSource? LowerBound { get; init; } = lowerBound; + public ValueSource? UpperBound { get; init; } = upperBound; + public RangeBounds RangeBound { get; } = rangeBound; + } diff --git a/src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs b/src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs deleted file mode 100644 index fd37277a35..0000000000 --- a/src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs +++ /dev/null @@ -1,26 +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.ValueConditions; - -public class RangeBound(ValueSource valueSource, bool exclusive = false) -{ - - public static implicit operator RangeBound(T value) => RangeBound.Create(value); - public static implicit operator RangeBound(Func calculated) => RangeBound.Create(calculated); - - public static RangeBound Create(T value, string? description = null) - => new(new SimpleValueSource(value, description)); - - public static RangeBound Create(Func calculation, string? description = null) - => new(new CalculatedValueSource(calculation)); - - public static RangeBound Create(CliValueSymbol otherSymbol, Func calculation, string? description = null) - => new(new RelativeToSymbolValueSource(otherSymbol, calculation, description)); - - public static RangeBound Create(string environmentVariableName, Func calculation, string? description = null) - => new(new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description)); - - public ValueSource ValueSource { get; } = valueSource; - public bool Exclusive { get; } = exclusive; -} diff --git a/src/System.CommandLine.Subsystems/ValueConditions/RangeBounds.cs b/src/System.CommandLine.Subsystems/ValueConditions/RangeBounds.cs new file mode 100644 index 0000000000..e63e5ab6ab --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueConditions/RangeBounds.cs @@ -0,0 +1,13 @@ +// 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.ValueConditions; + +[Flags] +public enum RangeBounds +{ + Inclusive = 0, + ExclusiveLowerBound = 1, + ExclusiveUpperBound = 2, + ExclusiveUpperAndLowerBounds = 3, +} diff --git a/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs index 81cbb9ee3c..dad3f8e34a 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs @@ -25,6 +25,17 @@ public abstract class ValueSource : ValueSource public static implicit operator ValueSource(T value) => new SimpleValueSource(value); public static implicit operator ValueSource(Func calculated) => new CalculatedValueSource(calculated); + public static ValueSource Create(T value, string? description = null) + => new SimpleValueSource(value, description); + + public static ValueSource Create(Func calculation, string? description = null) + => new CalculatedValueSource(calculation); + + public static ValueSource Create(CliValueSymbol otherSymbol, Func calculation, string? description = null) + => new RelativeToSymbolValueSource(otherSymbol, calculation, description); + + public static ValueSource Create(string environmentVariableName, Func calculation, string? description = null) + => new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description); } public class SimpleValueSource(T value, string? description = null) From 28a89e2d6be2fdffde845e310c0d5df95044e411 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Thu, 22 Aug 2024 10:55:12 -0400 Subject: [PATCH 134/150] Added tests and got them working --- ...System.CommandLine.Subsystems.Tests.csproj | 1 + .../ValidationSubsystemTests.cs | 48 ------ .../ValueSourceTests.cs | 156 ++++++++++++++++++ .../PipelineResult.cs | 6 +- .../ValueConditions/ValueSource.cs | 57 +++++-- .../ValueProvider.cs | 8 +- 6 files changed, 206 insertions(+), 70 deletions(-) create mode 100644 src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs diff --git a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj index 2067a4a2ac..ba321497f8 100644 --- a/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj +++ b/src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj @@ -32,6 +32,7 @@ --> + diff --git a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs index e985b04554..acb074cafc 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs @@ -185,52 +185,4 @@ public void Values_above_relative_upper_bound_report_error() var error = pipelineResult.GetErrors().First(); // TODO: Create test mechanism for CliDiagnostics } - - [Fact] - public void Values_below_environment_lower_bound_report_error() - { - var envName = "SYSTEM_COMMANDLINE_LOWERBOUND"; - Environment.SetEnvironmentVariable(envName, "2"); - var option = GetOptionWithRangeBounds(ValueSource.Create(envName, s => int.Parse(s) + 1), 50); - - var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 2"); - Environment.SetEnvironmentVariable(envName, null); - - pipelineResult.Should().NotBeNull(); - pipelineResult.GetErrors().Should().HaveCount(1); - var error = pipelineResult.GetErrors().First(); - // TODO: Create test mechanism for CliDiagnostics - } - - - [Fact] - public void Values_within_environment_range_do_not_report_error() - { - var envName = "SYSTEM_COMMANDLINE_LOWERBOUND"; - Environment.SetEnvironmentVariable(envName, "2"); - var option = GetOptionWithRangeBounds(ValueSource.Create(envName, s => int.Parse(s) + 1), 50); - - var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 11"); - Environment.SetEnvironmentVariable(envName, null); - - pipelineResult.Should().NotBeNull(); - pipelineResult.GetErrors().Should().BeEmpty(); - } - - [Fact] - public void Values_above_environment_upper_bound_report_error() - { - var envName = "SYSTEM_COMMANDLINE_LOWERBOUND"; - Environment.SetEnvironmentVariable(envName, "2"); - var option = GetOptionWithRangeBounds(0, ValueSource.Create(envName, s => int.Parse(s) + 1)); - - var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 4"); - Environment.SetEnvironmentVariable(envName, null); - - pipelineResult.Should().NotBeNull(); - pipelineResult.GetErrors().Should().HaveCount(1); - var error = pipelineResult.GetErrors().First(); - // TODO: Create test mechanism for CliDiagnostics - } - } diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs new file mode 100644 index 0000000000..3dd610adec --- /dev/null +++ b/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs @@ -0,0 +1,156 @@ +// 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 FluentAssertions; +using Microsoft.VisualBasic.FileIO; +using System.CommandLine.Parsing; +using System.CommandLine.ValueConditions; +using Xunit; + +namespace System.CommandLine.Subsystems.Tests; + +public class ValueSourceTests +{ + private PipelineResult EmptyPipelineResult(string input = "", params CliValueSymbol[] valueSymbols) + { + var rootCommand = new CliRootCommand(); + foreach (var symbol in valueSymbols) + { + rootCommand.Add(symbol); + } + var parseResult = CliParser.Parse(rootCommand, input); + return new PipelineResult(parseResult, "", Pipeline.CreateEmpty()); + } + + [Fact] + public void SimpleValueSource_with_set_value_retrieved() + { + var valueSource = new SimpleValueSource(42); + + int value = valueSource.GetTypedValue(EmptyPipelineResult()); + + value.Should() + .Be(42); + } + + [Fact] + public void SimpleValueSource_with_converted_value_retrieved() + { + ValueSource valueSource = 42; + + int value = valueSource.GetTypedValue(EmptyPipelineResult()); + + value.Should() + .Be(42); + } + + [Fact] + public void SimpleValueSource_created_via_extension_value_retrieved() + { + var valueSource = ValueSource.Create(42); + + int value = valueSource.GetTypedValue(EmptyPipelineResult()); + + value.Should() + .Be(42); + } + + [Fact] + public void CalculatedValueSource_produces_value() + { + var valueSource = new CalculatedValueSource(() => 42); + + int value = valueSource.GetTypedValue(EmptyPipelineResult()); + + value.Should() + .Be(42); + } + + [Fact] + public void CalculatedValueSource_implicitly_converted_produces_value() + { + // TODO: Figure out why this doesn't work, and remove implicit operator if it does not work + // ValueSource valueSource2 = (() => 42); + ValueSource valueSource = (ValueSource)(() => 42); + + int value = valueSource.GetTypedValue(EmptyPipelineResult()); + + value.Should() + .Be(42); + } + + [Fact] + public void CalculatedValueSource_from_extension_produces_value() + { + var valueSource = ValueSource.Create(() => 42); + int value = valueSource.GetTypedValue(EmptyPipelineResult()); + + value.Should() + .Be(42); + } + + [Fact] + public void RelativeToSymbolValueSource_produces_value_that_was_set() + { + var option = new CliOption("-a"); + var valueSource = new RelativeToSymbolValueSource(option); + + int value = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); + + value.Should() + .Be(42); + } + + [Fact] + public void RelativeToSymbolValueSource_implicitly_converted_produces_value_that_was_set() + { + var option = new CliOption("-a"); + ValueSource valueSource = option; + + int value = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); + + value.Should() + .Be(42); + } + + [Fact] + public void RelativeToSymbolValueSource_from_extension_produces_value_that_was_set() + { + var option = new CliOption("-a"); + var valueSource = new RelativeToSymbolValueSource(option); + + int value = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); + + value.Should() + .Be(42); + } + + [Fact] + public void RelativeToEnvironmentVariableValueSource_produces_value_that_was_set() + { + var envName = "SYSTEM_COMMANDLINE_TESTING"; + var valueSource = new RelativeToEnvironmentVariableValueSource(envName); + + Environment.SetEnvironmentVariable(envName, "42"); + int value = valueSource.GetTypedValue(EmptyPipelineResult("")); + Environment.SetEnvironmentVariable(envName, null); + + value.Should() + .Be(42); + } + + + [Fact] + public void RelativeToEnvironmentVariableValueSource_from_extension_produces_value_that_was_set() + { + var envName = "SYSTEM_COMMANDLINE_TESTING"; + var valueSource = ValueSource.CreateFromEnvironmentVariable(envName); + + Environment.SetEnvironmentVariable(envName, "42"); + int value = valueSource.GetTypedValue(EmptyPipelineResult("")); + Environment.SetEnvironmentVariable(envName, null); + + value.Should() + .Be(42); + } +} diff --git a/src/System.CommandLine.Subsystems/PipelineResult.cs b/src/System.CommandLine.Subsystems/PipelineResult.cs index 733e4110fd..a0eeba0c38 100644 --- a/src/System.CommandLine.Subsystems/PipelineResult.cs +++ b/src/System.CommandLine.Subsystems/PipelineResult.cs @@ -20,11 +20,11 @@ public class PipelineResult(ParseResult parseResult, string rawInput, Pipeline? public bool AlreadyHandled { get; set; } public int ExitCode { get; set; } - public T? GetValue(CliValueSymbol dataSymbol) + public T GetValue(CliValueSymbol dataSymbol) => valueProvider.GetValue(dataSymbol); - public object? GetValue(CliValueSymbol option) - => valueProvider.GetValue(option); + public object GetValue(CliValueSymbol option) + => valueProvider.GetValue(option); public CliValueResult? GetValueResult(CliValueSymbol dataSymbol) => ParseResult.GetValueResult(dataSymbol); diff --git a/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs index dad3f8e34a..afd37767f9 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs @@ -11,6 +11,17 @@ public abstract class ValueSource // TODO: Should we use ToString() here? public abstract string Description { get; } + public static ValueSource Create(T value, string? description = null) + => new SimpleValueSource(value, description); + + public static ValueSource Create(Func calculation, string? description = null) + => new CalculatedValueSource(calculation); + + public static ValueSource Create(CliValueSymbol otherSymbol, Func? calculation = null, string? description = null) + => new RelativeToSymbolValueSource(otherSymbol, calculation, description); + + public static ValueSource CreateFromEnvironmentVariable(string environmentVariableName, Func? calculation = null, string? description = null) + => new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description); } public abstract class ValueSource : ValueSource @@ -24,18 +35,8 @@ public abstract class ValueSource : ValueSource public static implicit operator ValueSource(T value) => new SimpleValueSource(value); public static implicit operator ValueSource(Func calculated) => new CalculatedValueSource(calculated); - - public static ValueSource Create(T value, string? description = null) - => new SimpleValueSource(value, description); - - public static ValueSource Create(Func calculation, string? description = null) - => new CalculatedValueSource(calculation); - - public static ValueSource Create(CliValueSymbol otherSymbol, Func calculation, string? description = null) - => new RelativeToSymbolValueSource(otherSymbol, calculation, description); - - public static ValueSource Create(string environmentVariableName, Func calculation, string? description = null) - => new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description); + public static implicit operator ValueSource(CliValueSymbol symbol) => new RelativeToSymbolValueSource(symbol); + // Environment variable does not have an explicit operator, because converting to string was too broad } public class SimpleValueSource(T value, string? description = null) @@ -57,21 +58,45 @@ public override T GetTypedValue(PipelineResult pipelineResult) => calculation(); } -public class RelativeToSymbolValueSource(CliValueSymbol otherSymbol, Func calculation, string? description) +public class RelativeToSymbolValueSource(CliValueSymbol otherSymbol, + Func? calculation = null, + string? description = null) : ValueSource { public override string Description { get; } = description; public override T GetTypedValue(PipelineResult pipelineResult) - => calculation(pipelineResult.GetValue(otherSymbol)); + => calculation is null + ? pipelineResult.GetValue(otherSymbol) + : calculation(pipelineResult.GetValue(otherSymbol)); } -public class RelativeToEnvironmentVariableValueSource(string environmentVariableName, Func calculation, string? description) +public class RelativeToEnvironmentVariableValueSource(string environmentVariableName, + Func? calculation = null, + string? description = null) : ValueSource { public override string Description { get; } = description; public override T GetTypedValue(PipelineResult pipelineResult) - => calculation(Environment.GetEnvironmentVariable(environmentVariableName)); + { + string? stringValue = Environment.GetEnvironmentVariable(environmentVariableName); + + if (stringValue is null) + { + // This feels wrong. It isn't saying "Hey, you asked for a value that was not there" + return default; + } + + // TODO: What is the best way to do this? + T value = default(T) switch + { + int i => (T)(object)Convert.ToInt32(stringValue), + _ => throw new NotImplementedException("Looking for a non-dumb way to do this") + }; + return calculation is null + ? value + : calculation(Environment.GetEnvironmentVariable(environmentVariableName)); + } } diff --git a/src/System.CommandLine.Subsystems/ValueProvider.cs b/src/System.CommandLine.Subsystems/ValueProvider.cs index af1984d5a3..ef098a6c42 100644 --- a/src/System.CommandLine.Subsystems/ValueProvider.cs +++ b/src/System.CommandLine.Subsystems/ValueProvider.cs @@ -34,11 +34,13 @@ private bool TryGetValue(CliSymbol symbol, out T? value) return false; } - public T? GetValue(CliValueSymbol valueSymbol) + public T GetValue(CliValueSymbol valueSymbol) => GetValueInternal(valueSymbol); - private T? GetValueInternal(CliValueSymbol? valueSymbol) + private T GetValueInternal(CliValueSymbol? valueSymbol) { + // TODO: This method is definitely WRONG. If there is a relative or env variable, it does not continue if it is not found + // TODO: Replace this method with an AggregateValueSource // NOTE: We use the subsystem's TryGetAnnotation here instead of the GetDefaultValue etc // extension methods, as the subsystem's TryGetAnnotation respects its annotation provider return valueSymbol switch @@ -59,7 +61,7 @@ private bool TryGetValue(CliSymbol symbol, out T? value) _ => UseValue(valueSymbol, default(T)) }; - TValue? UseValue(CliSymbol symbol, TValue? value) + TValue UseValue(CliSymbol symbol, TValue value) { SetValue(symbol, value); return value; From 3d60c6fd7f63f558d94d627104e54c7435f485e9 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Fri, 23 Aug 2024 08:48:31 -0400 Subject: [PATCH 135/150] Added success to ValueSource.GetValue and some reorg --- .../ValidationSubsystemTests.cs | 19 ++-- .../ValueSourceTests.cs | 53 ++++++--- .../ConsoleHelpers.cs | 2 - .../Directives/DiagramSubsystem.cs | 1 - .../SymbolAnnotationExtensions.cs | 3 - .../ValidationSubsystem.cs | 1 - .../ValueConditionAnnotationExtensions.cs | 4 +- .../ValueConditions/Range.cs | 9 +- .../ValueConditions/ValueSource.cs | 102 ------------------ .../ValueProvider.cs | 1 - .../ValueSources/AggregateValueSource.cs | 17 +++ .../ValueSources/CalculatedValueSource.cs | 15 +++ ...elativeToEnvironmentVariableValueSource.cs | 34 ++++++ .../RelativeToSymbolValueSource.cs | 18 ++++ .../ValueSources/SimpleValueSource.cs | 14 +++ .../ValueSources/ValueSource.cs | 39 +++++++ 16 files changed, 191 insertions(+), 141 deletions(-) delete mode 100644 src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs create mode 100644 src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs create mode 100644 src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs create mode 100644 src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs create mode 100644 src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs create mode 100644 src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs create mode 100644 src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs diff --git a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs index acb074cafc..89d70a5382 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs @@ -2,12 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using FluentAssertions; -using Microsoft.VisualBasic.FileIO; -using System.CommandLine.Directives; using System.CommandLine.Parsing; -using System.CommandLine.ValueConditions; +using System.CommandLine.ValueSources; using Xunit; -using static System.CommandLine.Subsystems.Tests.TestData; namespace System.CommandLine.Subsystems.Tests; @@ -107,7 +104,7 @@ public void Int_values_on_upper_range_bound_do_not_report_error() [Fact] public void Values_below_calculated_lower_bound_report_error() { - var option = GetOptionWithRangeBounds(ValueSource.Create(() => 1), 50); + var option = GetOptionWithRangeBounds(ValueSource.Create(() => (true, 1)), 50); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 0"); @@ -121,7 +118,7 @@ public void Values_below_calculated_lower_bound_report_error() [Fact] public void Values_within_calculated_range_do_not_report_error() { - var option = GetOptionWithRangeBounds(ValueSource.Create(() => 1), ValueSource.Create(() => 50)); + var option = GetOptionWithRangeBounds(ValueSource.Create(() => (true, 1)), ValueSource.Create(() => (true, 50))); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); @@ -132,7 +129,7 @@ public void Values_within_calculated_range_do_not_report_error() [Fact] public void Values_above_calculated_upper_bound_report_error() { - var option = GetOptionWithRangeBounds(0, ValueSource.Create(() => 40)); + var option = GetOptionWithRangeBounds(0, ValueSource.Create(() => (true, 40))); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); @@ -146,7 +143,7 @@ public void Values_above_calculated_upper_bound_report_error() public void Values_below_relative_lower_bound_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (int)o + 1), 50); + var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (true, (int)o + 1)), 50); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 0 -a 0"); @@ -162,7 +159,7 @@ public void Values_below_relative_lower_bound_report_error() public void Values_within_relative_range_do_not_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (int)o + 1), ValueSource.Create(otherOption, o => (int)o + 10)); + var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (true, (int)o + 1)), ValueSource.Create(otherOption, o => (true, (int)o + 10))); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 11 -a 3"); @@ -175,7 +172,7 @@ public void Values_within_relative_range_do_not_report_error() public void Values_above_relative_upper_bound_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds(0, ValueSource.Create(otherOption, o => (int)o + 10)); + var option = GetOptionWithRangeBounds(0, ValueSource.Create(otherOption, o => (true, (int)o + 10))); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 9 -a -2"); @@ -185,4 +182,6 @@ public void Values_above_relative_upper_bound_report_error() var error = pipelineResult.GetErrors().First(); // TODO: Create test mechanism for CliDiagnostics } + + } diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs index 3dd610adec..7eda2e9e49 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs @@ -5,6 +5,7 @@ using Microsoft.VisualBasic.FileIO; using System.CommandLine.Parsing; using System.CommandLine.ValueConditions; +using System.CommandLine.ValueSources; using Xunit; namespace System.CommandLine.Subsystems.Tests; @@ -27,8 +28,10 @@ public void SimpleValueSource_with_set_value_retrieved() { var valueSource = new SimpleValueSource(42); - int value = valueSource.GetTypedValue(EmptyPipelineResult()); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); + success.Should() + .BeTrue(); value.Should() .Be(42); } @@ -38,8 +41,10 @@ public void SimpleValueSource_with_converted_value_retrieved() { ValueSource valueSource = 42; - int value = valueSource.GetTypedValue(EmptyPipelineResult()); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); + success.Should() + .BeTrue(); value.Should() .Be(42); } @@ -49,8 +54,10 @@ public void SimpleValueSource_created_via_extension_value_retrieved() { var valueSource = ValueSource.Create(42); - int value = valueSource.GetTypedValue(EmptyPipelineResult()); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); + success.Should() + .BeTrue(); value.Should() .Be(42); } @@ -58,10 +65,12 @@ public void SimpleValueSource_created_via_extension_value_retrieved() [Fact] public void CalculatedValueSource_produces_value() { - var valueSource = new CalculatedValueSource(() => 42); + var valueSource = new CalculatedValueSource(() => (true, 42)); - int value = valueSource.GetTypedValue(EmptyPipelineResult()); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); + success.Should() + .BeTrue(); value.Should() .Be(42); } @@ -71,10 +80,12 @@ public void CalculatedValueSource_implicitly_converted_produces_value() { // TODO: Figure out why this doesn't work, and remove implicit operator if it does not work // ValueSource valueSource2 = (() => 42); - ValueSource valueSource = (ValueSource)(() => 42); + ValueSource valueSource = (ValueSource)(() => (true, 42)); ; - int value = valueSource.GetTypedValue(EmptyPipelineResult()); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); + success.Should() + .BeTrue(); value.Should() .Be(42); } @@ -82,9 +93,11 @@ public void CalculatedValueSource_implicitly_converted_produces_value() [Fact] public void CalculatedValueSource_from_extension_produces_value() { - var valueSource = ValueSource.Create(() => 42); - int value = valueSource.GetTypedValue(EmptyPipelineResult()); + var valueSource = ValueSource.Create(() => (true, 42)); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); + success.Should() + .BeTrue(); value.Should() .Be(42); } @@ -95,8 +108,10 @@ public void RelativeToSymbolValueSource_produces_value_that_was_set() var option = new CliOption("-a"); var valueSource = new RelativeToSymbolValueSource(option); - int value = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); + success.Should() + .BeTrue(); value.Should() .Be(42); } @@ -107,8 +122,10 @@ public void RelativeToSymbolValueSource_implicitly_converted_produces_value_that var option = new CliOption("-a"); ValueSource valueSource = option; - int value = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); + success.Should() + .BeTrue(); value.Should() .Be(42); } @@ -119,8 +136,10 @@ public void RelativeToSymbolValueSource_from_extension_produces_value_that_was_s var option = new CliOption("-a"); var valueSource = new RelativeToSymbolValueSource(option); - int value = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); + success.Should() + .BeTrue(); value.Should() .Be(42); } @@ -132,9 +151,11 @@ public void RelativeToEnvironmentVariableValueSource_produces_value_that_was_set var valueSource = new RelativeToEnvironmentVariableValueSource(envName); Environment.SetEnvironmentVariable(envName, "42"); - int value = valueSource.GetTypedValue(EmptyPipelineResult("")); - Environment.SetEnvironmentVariable(envName, null); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult("")); + Environment.SetEnvironmentVariable(envName, null); + success.Should() + .BeTrue(); value.Should() .Be(42); } @@ -147,9 +168,11 @@ public void RelativeToEnvironmentVariableValueSource_from_extension_produces_val var valueSource = ValueSource.CreateFromEnvironmentVariable(envName); Environment.SetEnvironmentVariable(envName, "42"); - int value = valueSource.GetTypedValue(EmptyPipelineResult("")); + (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult("")); Environment.SetEnvironmentVariable(envName, null); + success.Should() + .BeTrue(); value.Should() .Be(42); } diff --git a/src/System.CommandLine.Subsystems/ConsoleHelpers.cs b/src/System.CommandLine.Subsystems/ConsoleHelpers.cs index 2684413c89..8470bb5f0f 100644 --- a/src/System.CommandLine.Subsystems/ConsoleHelpers.cs +++ b/src/System.CommandLine.Subsystems/ConsoleHelpers.cs @@ -1,8 +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.Runtime.InteropServices; - namespace System.CommandLine { internal static class ConsoleHelpers diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 352d960e54..791290a097 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -3,7 +3,6 @@ using System.CommandLine.Subsystems; using System.Text; -using System.CommandLine.Parsing; namespace System.CommandLine.Directives; diff --git a/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs index 53e2fb5117..87a19c9da0 100644 --- a/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/SymbolAnnotationExtensions.cs @@ -1,9 +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.CommandLine.Subsystems; -using System.CommandLine.Subsystems.Annotations; - namespace System.CommandLine; /// diff --git a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs index 7b77a6ec70..076ec829b3 100644 --- a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValidationSubsystem.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.CommandLine; using System.CommandLine.Parsing; using System.CommandLine.Subsystems; using System.CommandLine.Validation; diff --git a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs index 7082f449bc..76928d68af 100644 --- a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs @@ -1,6 +1,6 @@ -using System.CommandLine; -using System.CommandLine.Subsystems.Annotations; +using System.CommandLine.Subsystems.Annotations; using System.CommandLine.ValueConditions; +using System.CommandLine.ValueSources; namespace System.CommandLine; diff --git a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs index b511fa2d32..6334f8543f 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs @@ -3,6 +3,7 @@ using System.CommandLine.Parsing; using System.CommandLine.Validation; +using System.CommandLine.ValueSources; namespace System.CommandLine.ValueConditions; @@ -34,8 +35,8 @@ public void Validate(object? value, // TODO: Replace the strings we are comparing with a diagnostic ID when we update ParseError if (LowerBound is not null) { - var lowerValue = LowerBound.GetTypedValue(validationContext.PipelineResult); - if (comparableValue.CompareTo(lowerValue) < 0) + var lower = LowerBound.GetTypedValue(validationContext.PipelineResult); + if (lower.success && comparableValue.CompareTo(lower.value) < 0) { validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is below the lower bound of {LowerBound}")); } @@ -43,8 +44,8 @@ public void Validate(object? value, if (UpperBound is not null) { - var upperValue = UpperBound.GetTypedValue(validationContext.PipelineResult); - if (comparableValue.CompareTo(upperValue) > 0) + var upper = UpperBound.GetTypedValue(validationContext.PipelineResult); + if (upper.success && comparableValue.CompareTo(upper.value) > 0) { validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {UpperBound}")); } diff --git a/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs deleted file mode 100644 index afd37767f9..0000000000 --- a/src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs +++ /dev/null @@ -1,102 +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 static System.Runtime.InteropServices.JavaScript.JSType; - -namespace System.CommandLine.ValueConditions; - -public abstract class ValueSource -{ - public abstract object? GetValue(PipelineResult pipelineResult); - - // TODO: Should we use ToString() here? - public abstract string Description { get; } - public static ValueSource Create(T value, string? description = null) - => new SimpleValueSource(value, description); - - public static ValueSource Create(Func calculation, string? description = null) - => new CalculatedValueSource(calculation); - - public static ValueSource Create(CliValueSymbol otherSymbol, Func? calculation = null, string? description = null) - => new RelativeToSymbolValueSource(otherSymbol, calculation, description); - - public static ValueSource CreateFromEnvironmentVariable(string environmentVariableName, Func? calculation = null, string? description = null) - => new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description); -} - -public abstract class ValueSource : ValueSource -{ - public abstract T GetTypedValue(PipelineResult pipelineResult); - - public override object? GetValue(PipelineResult pipelineResult) - { - return GetTypedValue(pipelineResult); - } - - public static implicit operator ValueSource(T value) => new SimpleValueSource(value); - public static implicit operator ValueSource(Func calculated) => new CalculatedValueSource(calculated); - public static implicit operator ValueSource(CliValueSymbol symbol) => new RelativeToSymbolValueSource(symbol); - // Environment variable does not have an explicit operator, because converting to string was too broad -} - -public class SimpleValueSource(T value, string? description = null) - : ValueSource -{ - public override string Description { get; } = description; - - public override T GetTypedValue(PipelineResult pipelineResult) - => value; -} - -// Find an example of when this is useful beyond Random and Guid. Is a time lag between building the CLI and validating important (DateTime.Now()) -public class CalculatedValueSource(Func calculation, string? description = null) - : ValueSource -{ - public override string Description { get; } = description; - - public override T GetTypedValue(PipelineResult pipelineResult) - => calculation(); -} - -public class RelativeToSymbolValueSource(CliValueSymbol otherSymbol, - Func? calculation = null, - string? description = null) - : ValueSource -{ - public override string Description { get; } = description; - - public override T GetTypedValue(PipelineResult pipelineResult) - => calculation is null - ? pipelineResult.GetValue(otherSymbol) - : calculation(pipelineResult.GetValue(otherSymbol)); -} - -public class RelativeToEnvironmentVariableValueSource(string environmentVariableName, - Func? calculation = null, - string? description = null) - : ValueSource -{ - public override string Description { get; } = description; - - public override T GetTypedValue(PipelineResult pipelineResult) - { - string? stringValue = Environment.GetEnvironmentVariable(environmentVariableName); - - if (stringValue is null) - { - // This feels wrong. It isn't saying "Hey, you asked for a value that was not there" - return default; - } - - // TODO: What is the best way to do this? - T value = default(T) switch - { - int i => (T)(object)Convert.ToInt32(stringValue), - _ => throw new NotImplementedException("Looking for a non-dumb way to do this") - }; - return calculation is null - ? value - : calculation(Environment.GetEnvironmentVariable(environmentVariableName)); - } -} - diff --git a/src/System.CommandLine.Subsystems/ValueProvider.cs b/src/System.CommandLine.Subsystems/ValueProvider.cs index ef098a6c42..9f9eb9f609 100644 --- a/src/System.CommandLine.Subsystems/ValueProvider.cs +++ b/src/System.CommandLine.Subsystems/ValueProvider.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.CommandLine.Subsystems; using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine; diff --git a/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs new file mode 100644 index 0000000000..89dc66745b --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs @@ -0,0 +1,17 @@ +// 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.ValueSources; + +public class AggregateValueSource : ValueSource +{ + private List valueSources = []; + + public override string Description { get; } + + public override (bool success, object? value) GetValue(PipelineResult pipelineResult) + { + throw new NotImplementedException(); + } +} + diff --git a/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs new file mode 100644 index 0000000000..c2e7164fd9 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs @@ -0,0 +1,15 @@ +// 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.ValueSources; + +// Find an example of when this is useful beyond Random and Guid. Is a time lag between building the CLI and validating important (DateTime.Now()) +public class CalculatedValueSource(Func<(bool success, T? value)> calculation, string? description = null) + : ValueSource +{ + public override string Description { get; } = description; + + public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) + => calculation(); +} + diff --git a/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs new file mode 100644 index 0000000000..1123d387b8 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs @@ -0,0 +1,34 @@ +// 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.ValueSources; + +public class RelativeToEnvironmentVariableValueSource(string environmentVariableName, + Func? calculation = null, + string? description = null) + : ValueSource +{ + public override string Description { get; } = description; + + public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) + { + string? stringValue = Environment.GetEnvironmentVariable(environmentVariableName); + + if (stringValue is null) + { + // This feels wrong. It isn't saying "Hey, you asked for a value that was not there" + return default; + } + + // TODO: What is the best way to do this? + T value = default(T) switch + { + int i => (T)(object)Convert.ToInt32(stringValue), + _ => throw new NotImplementedException("Looking for a non-dumb way to do this") + }; + return calculation is null + ? (true, value) + : calculation(Environment.GetEnvironmentVariable(environmentVariableName)); + } +} + diff --git a/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs new file mode 100644 index 0000000000..ed69b3c785 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs @@ -0,0 +1,18 @@ +// 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.ValueSources; + +public class RelativeToSymbolValueSource(CliValueSymbol otherSymbol, + Func? calculation = null, + string? description = null) + : ValueSource +{ + public override string Description { get; } = description; + + public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) + => calculation is null + ? (true, pipelineResult.GetValue(otherSymbol)) + : calculation(pipelineResult.GetValue(otherSymbol)); +} + diff --git a/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs new file mode 100644 index 0000000000..2d500f15a1 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs @@ -0,0 +1,14 @@ +// 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.ValueSources; + +public class SimpleValueSource(T value, string? description = null) + : ValueSource +{ + public override string Description { get; } = description; + + public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) + => (true, value); +} + diff --git a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs new file mode 100644 index 0000000000..e8e91a7643 --- /dev/null +++ b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs @@ -0,0 +1,39 @@ +// 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.ValueSources; + +public abstract class ValueSource +{ + public abstract (bool success, object? value) GetValue(PipelineResult pipelineResult); + + // TODO: Should we use ToString() here? + public abstract string Description { get; } + public static ValueSource Create(T value, string? description = null) + => new SimpleValueSource(value, description); + + public static ValueSource Create(Func<(bool success, T? value)> calculation, string? description = null) + => new CalculatedValueSource(calculation); + + public static ValueSource Create(CliValueSymbol otherSymbol, Func? calculation = null, string? description = null) + => new RelativeToSymbolValueSource(otherSymbol, calculation, description); + + public static ValueSource CreateFromEnvironmentVariable(string environmentVariableName, Func? calculation = null, string? description = null) + => new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description); +} + +public abstract class ValueSource : ValueSource +{ + public abstract (bool success, T? value) GetTypedValue(PipelineResult pipelineResult); + + public override (bool success, object? value) GetValue(PipelineResult pipelineResult) + { + return GetTypedValue(pipelineResult); + } + + public static implicit operator ValueSource(T value) => new SimpleValueSource(value); + public static implicit operator ValueSource(Func<(bool success, T? value)> calculated) => new CalculatedValueSource(calculated); + public static implicit operator ValueSource(CliValueSymbol symbol) => new RelativeToSymbolValueSource(symbol); + // Environment variable does not have an explicit operator, because converting to string was too broad +} + From 1e120e3b78cecfc4850225d3ec667416a2a291a0 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Fri, 23 Aug 2024 08:51:40 -0400 Subject: [PATCH 136/150] Made ValueSource.Description nullable --- src/System.CommandLine.Subsystems/Validation/RangeValidator.cs | 2 +- .../ValueSources/AggregateValueSource.cs | 2 +- .../ValueSources/CalculatedValueSource.cs | 2 +- .../ValueSources/RelativeToEnvironmentVariableValueSource.cs | 2 +- .../ValueSources/RelativeToSymbolValueSource.cs | 2 +- .../ValueSources/SimpleValueSource.cs | 2 +- src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs b/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs index 10c0fa0293..e75429156c 100644 --- a/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs @@ -20,7 +20,7 @@ public override void Validate(object? value, CliValueSymbol valueSymbol, } if (valueCondition.MustHaveValidator) { - validationContext.PipelineResult.AddError(new ParseError($"Range validator missing for {valueResult.ValueSymbol.Name}")); + validationContext.PipelineResult.AddError(new ParseError($"Range validator missing for {valueSymbol.Name}")); } } diff --git a/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs index 89dc66745b..d103eb5b31 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs @@ -7,7 +7,7 @@ public class AggregateValueSource : ValueSource { private List valueSources = []; - public override string Description { get; } + public override string? Description { get; } public override (bool success, object? value) GetValue(PipelineResult pipelineResult) { diff --git a/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs index c2e7164fd9..6cf25b4c28 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs @@ -7,7 +7,7 @@ namespace System.CommandLine.ValueSources; public class CalculatedValueSource(Func<(bool success, T? value)> calculation, string? description = null) : ValueSource { - public override string Description { get; } = description; + public override string? Description { get; } = description; public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) => calculation(); diff --git a/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs index 1123d387b8..132504df94 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs @@ -8,7 +8,7 @@ public class RelativeToEnvironmentVariableValueSource(string environmentVaria string? description = null) : ValueSource { - public override string Description { get; } = description; + public override string? Description { get; } = description; public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) { diff --git a/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs index ed69b3c785..9595a6eb75 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs @@ -8,7 +8,7 @@ public class RelativeToSymbolValueSource(CliValueSymbol otherSymbol, string? description = null) : ValueSource { - public override string Description { get; } = description; + public override string? Description { get; } = description; public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) => calculation is null diff --git a/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs index 2d500f15a1..a4fcbae3d4 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs @@ -6,7 +6,7 @@ namespace System.CommandLine.ValueSources; public class SimpleValueSource(T value, string? description = null) : ValueSource { - public override string Description { get; } = description; + public override string? Description { get; } = description; public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) => (true, value); diff --git a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs index e8e91a7643..e6fe6d0bda 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs @@ -8,7 +8,7 @@ public abstract class ValueSource public abstract (bool success, object? value) GetValue(PipelineResult pipelineResult); // TODO: Should we use ToString() here? - public abstract string Description { get; } + public abstract string? Description { get; } public static ValueSource Create(T value, string? description = null) => new SimpleValueSource(value, description); From b92fbfa032aebec6fc9b9ad0eb50a6ec06fc3fdf Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Fri, 23 Aug 2024 15:53:03 -0400 Subject: [PATCH 137/150] Implmented aggregate value and updated ValueProvider --- .../PipelineResult.cs | 26 ++++--- .../Annotations/ValueAnnotations.cs | 6 ++ .../ValueAnnotationExtensions.cs | 27 ++++++- .../ValueProvider.cs | 71 +++++++++++++------ .../ValueSources/AggregateValueSource.cs | 48 +++++++++++-- .../ValueSources/ValueSource.cs | 5 ++ 6 files changed, 147 insertions(+), 36 deletions(-) diff --git a/src/System.CommandLine.Subsystems/PipelineResult.cs b/src/System.CommandLine.Subsystems/PipelineResult.cs index a0eeba0c38..e9972f1736 100644 --- a/src/System.CommandLine.Subsystems/PipelineResult.cs +++ b/src/System.CommandLine.Subsystems/PipelineResult.cs @@ -5,25 +5,35 @@ namespace System.CommandLine; -public class PipelineResult(ParseResult parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) +public class PipelineResult { // TODO: Try to build workflow so it is illegal to create this without a ParseResult private readonly List errors = []; - public ParseResult ParseResult { get; } = parseResult; - private ValueProvider valueProvider { get; } = new ValueProvider(parseResult); - public string RawInput { get; } = rawInput; + private ValueProvider valueProvider { get; } + + public PipelineResult(ParseResult parseResult, string rawInput, Pipeline? pipeline, ConsoleHack? consoleHack = null) + { + ParseResult = parseResult; + RawInput = rawInput; + Pipeline = pipeline ?? Pipeline.CreateEmpty(); + ConsoleHack = consoleHack ?? new ConsoleHack(); + valueProvider = new ValueProvider(this); + } + + public ParseResult ParseResult { get; } + public string RawInput { get; } // TODO: Consider behavior when pipeline is null - this is probably a core user accessing some subsystems - public Pipeline Pipeline { get; } = pipeline ?? Pipeline.CreateEmpty(); - public ConsoleHack ConsoleHack { get; } = consoleHack ?? new ConsoleHack(); + public Pipeline Pipeline { get; } + public ConsoleHack ConsoleHack { get; } public bool AlreadyHandled { get; set; } public int ExitCode { get; set; } - public T GetValue(CliValueSymbol dataSymbol) + public T? GetValue(CliValueSymbol dataSymbol) => valueProvider.GetValue(dataSymbol); - public object GetValue(CliValueSymbol option) + public object? GetValue(CliValueSymbol option) => valueProvider.GetValue(option); public CliValueResult? GetValueResult(CliValueSymbol dataSymbol) diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs index ce95549067..0e8088cb64 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs @@ -10,6 +10,11 @@ public static class ValueAnnotations { internal static string Prefix { get; } = nameof(SubsystemKind.Value); + /// + /// Default value source, which may be an aggregate source, for an option or argument + /// + public static AnnotationId DefaultValueSource { get; } = new(Prefix, nameof(DefaultValueSource)); + /// /// Default value for an option or argument /// @@ -19,6 +24,7 @@ public static class ValueAnnotations /// public static AnnotationId DefaultValue { get; } = new(Prefix, nameof(DefaultValue)); + /// /// Default value calculation for an option or argument /// diff --git a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs index d7b936a837..6b00ed3bec 100644 --- a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs @@ -3,12 +3,37 @@ using System.CommandLine.Subsystems; using System.CommandLine.Subsystems.Annotations; +using System.CommandLine.ValueSources; +using System.Diagnostics.CodeAnalysis; namespace System.CommandLine; public static class ValueAnnotationExtensions { - + + /// + /// Get the default value annotation for the + /// + /// The type of the option value + /// The option + /// The option's default value annotation if any, otherwise + /// + /// This is intended to be called by CLI authors. Subsystems should instead call , + /// which calculates the actual default value, based on the default value annotation and default value calculation, + /// whether directly stored on the symbol or from the subsystem's . + /// + public static bool TryGetDefaultValueSource(this CliValueSymbol valueSymbol, [NotNullWhen(true)] out ValueSource? defaultValueSource) + => valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValueSource, out defaultValueSource); + + /// + /// Sets the default value annotation on the + /// + /// The type of the option value + /// The option + /// The default value for the option + public static void SetDefaultValueSource(this CliValueSymbol valueSymbol, ValueSource defaultValue) + => valueSymbol.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); + /// /// Get the default value annotation for the /// diff --git a/src/System.CommandLine.Subsystems/ValueProvider.cs b/src/System.CommandLine.Subsystems/ValueProvider.cs index 9f9eb9f609..50043cdeee 100644 --- a/src/System.CommandLine.Subsystems/ValueProvider.cs +++ b/src/System.CommandLine.Subsystems/ValueProvider.cs @@ -2,17 +2,19 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Subsystems.Annotations; +using System.CommandLine.ValueSources; +using System.Diagnostics.CodeAnalysis; namespace System.CommandLine; internal class ValueProvider { private Dictionary cachedValues = []; - private ParseResult? parseResult = null; + private PipelineResult pipelineResult; - public ValueProvider(ParseResult parseResult) + public ValueProvider(PipelineResult pipelineResult) { - this.parseResult = parseResult; + this.pipelineResult = pipelineResult; } private void SetValue(CliSymbol symbol, object? value) @@ -33,32 +35,55 @@ private bool TryGetValue(CliSymbol symbol, out T? value) return false; } - public T GetValue(CliValueSymbol valueSymbol) + public T? GetValue(CliValueSymbol valueSymbol) => GetValueInternal(valueSymbol); - private T GetValueInternal(CliValueSymbol? valueSymbol) + private T? GetValueInternal(CliValueSymbol valueSymbol) { - // TODO: This method is definitely WRONG. If there is a relative or env variable, it does not continue if it is not found + var _ = valueSymbol ?? throw new ArgumentNullException(nameof(valueSymbol)); + if (TryGetValue(valueSymbol, out var value)) + { + return value; + } + if (pipelineResult.ParseResult?.GetValueResult(valueSymbol) is { } valueResult) + { + return UseValue(valueSymbol, valueResult.GetValue()); + } + if (valueSymbol.TryGetDefaultValueSource(out ValueSource? defaultValueSource)) + { + if (defaultValueSource is not ValueSource typedValueSource) + { + throw new InvalidOperationException("Unexpected ValueSource type"); + } + (var success, var defaultValue) = typedValueSource.GetTypedValue(pipelineResult); + if (success) + { + return UseValue(valueSymbol, defaultValue); + } + } + return UseValue(valueSymbol, default(T)); + + // TODO: The following logic is definitely WRONG. If there is a relative or env variable, it does not continue if it is not found // TODO: Replace this method with an AggregateValueSource // NOTE: We use the subsystem's TryGetAnnotation here instead of the GetDefaultValue etc // extension methods, as the subsystem's TryGetAnnotation respects its annotation provider - return valueSymbol switch - { - { } when TryGetValue(valueSymbol, out var value) - => value, // It has already been retrieved at least once - { } when parseResult?.GetValueResult(valueSymbol) is { } valueResult // GetValue not used because it would always return a value - => UseValue(valueSymbol, valueResult.GetValue()), // Value was supplied during parsing, - // Value was not supplied during parsing, determine default now - // configuration values go here in precedence - //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) - // => UseValue(symbol, GetEnvByName(envName)), - { } when valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation) - => UseValue(valueSymbol, CalculatedDefault(valueSymbol, (Func)defaultValueCalculation)), - { } when valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValue, out T? explicitValue) - => UseValue(valueSymbol, explicitValue), - null => throw new ArgumentNullException(nameof(valueSymbol)), - _ => UseValue(valueSymbol, default(T)) - }; + //return valueSymbol switch + //{ + // { } when TryGetValue(valueSymbol, out var value) + // => value, // It has already been retrieved at least once + // { } when parseResult?.GetValueResult(valueSymbol) is { } valueResult // GetValue not used because it would always return a value + // => UseValue(valueSymbol, valueResult.GetValue()), // Value was supplied during parsing, + // // Value was not supplied during parsing, determine default now + // // configuration values go here in precedence + // //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) + // // => UseValue(symbol, GetEnvByName(envName)), + // { } when valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation) + // => UseValue(valueSymbol, CalculatedDefault(valueSymbol, (Func)defaultValueCalculation)), + // { } when valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValue, out T? explicitValue) + // => UseValue(valueSymbol, explicitValue), + // null => throw new ArgumentNullException(nameof(valueSymbol)), + // _ => UseValue(valueSymbol, default(T)) + //}; TValue UseValue(CliSymbol symbol, TValue value) { diff --git a/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs index d103eb5b31..f3aff61d88 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs @@ -3,15 +3,55 @@ namespace System.CommandLine.ValueSources; -public class AggregateValueSource : ValueSource +public class AggregateValueSource : ValueSource { - private List valueSources = []; + private List> valueSources = []; + + public AggregateValueSource(ValueSource firstSource, + ValueSource secondSource, + string? description = null, + params ValueSource[] otherSources) + { + valueSources.AddRange([firstSource, secondSource, .. otherSources]); + Description = description; + } + public override string? Description { get; } - public override (bool success, object? value) GetValue(PipelineResult pipelineResult) + public bool PrecedenceAsEntered { get; set; } + + public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) + => ValueFromSources(pipelineResult); + + private (bool success, T? value) ValueFromSources(PipelineResult pipelineResult) { - throw new NotImplementedException(); + var orderedSources = PrecedenceAsEntered + ? valueSources + : [.. valueSources.OrderBy(GetPrecedence)]; + foreach (var source in orderedSources) + { + (var success, var value) = source.GetTypedValue(pipelineResult); + if (success) + { + return (true, value); + } + } + return (false, default); + } + + internal static int GetPrecedence(ValueSource source) + { + return source switch + { + SimpleValueSource => 0, + CalculatedValueSource => 1, + RelativeToSymbolValueSource => 2, + //RelativeToConfigurationValueSource => 3, + RelativeToEnvironmentVariableValueSource => 4, + _ => 5 + }; } } + diff --git a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs index e6fe6d0bda..cd2f547556 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs @@ -18,6 +18,11 @@ public static ValueSource Create(Func<(bool success, T? value)> calculatio public static ValueSource Create(CliValueSymbol otherSymbol, Func? calculation = null, string? description = null) => new RelativeToSymbolValueSource(otherSymbol, calculation, description); + public static ValueSource Create(ValueSource firstSource, ValueSource secondSource, string? description = null, params ValueSource[] otherSources) + { + return new AggregateValueSource(firstSource, secondSource, description, otherSources); + } + public static ValueSource CreateFromEnvironmentVariable(string environmentVariableName, Func? calculation = null, string? description = null) => new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description); } From 993ea1e333b5adf81e7bf44dc1d1f690d9cb05b8 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 24 Aug 2024 09:10:36 -0400 Subject: [PATCH 138/150] Changed to TryGet... in ValueSources for consistency --- .../ValidationSubsystemTests.cs | 2 +- .../ValueSourceTests.cs | 149 ++++++++++-------- .../ValueConditions/Range.cs | 21 ++- .../ValueProvider.cs | 5 +- .../ValueSources/AggregateValueSource.cs | 16 +- .../ValueSources/CalculatedValueSource.cs | 13 +- ...elativeToEnvironmentVariableValueSource.cs | 38 +++-- .../RelativeToSymbolValueSource.cs | 31 +++- .../ValueSources/SimpleValueSource.cs | 8 +- .../ValueSources/ValueSource.cs | 46 ++++-- 10 files changed, 200 insertions(+), 129 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs index 89d70a5382..3ca4e362c4 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs @@ -143,7 +143,7 @@ public void Values_above_calculated_upper_bound_report_error() public void Values_below_relative_lower_bound_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (true, (int)o + 1)), 50); + var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (true, (int)o + 1)), 50); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 0 -a 0"); diff --git a/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs b/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs index 7eda2e9e49..68a482b6b7 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValueSourceTests.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using FluentAssertions; -using Microsoft.VisualBasic.FileIO; using System.CommandLine.Parsing; -using System.CommandLine.ValueConditions; using System.CommandLine.ValueSources; using Xunit; @@ -28,12 +26,13 @@ public void SimpleValueSource_with_set_value_retrieved() { var valueSource = new SimpleValueSource(42); - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); - - success.Should() - .BeTrue(); - value.Should() - .Be(42); + if (valueSource.TryGetTypedValue(EmptyPipelineResult(), out var value)) + { + value.Should() + .Be(42); + return; + } + Assert.Fail("Typed value not retrieved"); } [Fact] @@ -41,12 +40,13 @@ public void SimpleValueSource_with_converted_value_retrieved() { ValueSource valueSource = 42; - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); - - success.Should() - .BeTrue(); - value.Should() - .Be(42); + if (valueSource.TryGetTypedValue(EmptyPipelineResult(), out var value)) + { + value.Should() + .Be(42); + return; + } + Assert.Fail("Typed value not retrieved"); } [Fact] @@ -54,12 +54,13 @@ public void SimpleValueSource_created_via_extension_value_retrieved() { var valueSource = ValueSource.Create(42); - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); - - success.Should() - .BeTrue(); - value.Should() - .Be(42); + if (valueSource.TryGetTypedValue(EmptyPipelineResult(), out var value)) + { + value.Should() + .Be(42); + return; + } + Assert.Fail("Typed value not retrieved"); } [Fact] @@ -67,12 +68,13 @@ public void CalculatedValueSource_produces_value() { var valueSource = new CalculatedValueSource(() => (true, 42)); - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); - - success.Should() - .BeTrue(); - value.Should() - .Be(42); + if (valueSource.TryGetTypedValue(EmptyPipelineResult(), out var value)) + { + value.Should() + .Be(42); + return; + } + Assert.Fail("Typed value not retrieved"); } [Fact] @@ -82,24 +84,27 @@ public void CalculatedValueSource_implicitly_converted_produces_value() // ValueSource valueSource2 = (() => 42); ValueSource valueSource = (ValueSource)(() => (true, 42)); ; - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); - - success.Should() - .BeTrue(); - value.Should() - .Be(42); + if (valueSource.TryGetTypedValue(EmptyPipelineResult(), out var value)) + { + value.Should() + .Be(42); + return; + } + Assert.Fail("Typed value not retrieved"); } [Fact] public void CalculatedValueSource_from_extension_produces_value() { var valueSource = ValueSource.Create(() => (true, 42)); - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult()); - success.Should() - .BeTrue(); - value.Should() - .Be(42); + if (valueSource.TryGetTypedValue(EmptyPipelineResult(), out var value)) + { + value.Should() + .Be(42); + return; + } + Assert.Fail("Typed value not retrieved"); } [Fact] @@ -108,12 +113,14 @@ public void RelativeToSymbolValueSource_produces_value_that_was_set() var option = new CliOption("-a"); var valueSource = new RelativeToSymbolValueSource(option); - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); - - success.Should() - .BeTrue(); - value.Should() - .Be(42); + if (valueSource.TryGetTypedValue(EmptyPipelineResult("-a 42", option), out var value)) + { + value.Should() + .Be(42); + return; + } + Assert.Fail("Typed value not retrieved"); + } [Fact] @@ -122,12 +129,13 @@ public void RelativeToSymbolValueSource_implicitly_converted_produces_value_that var option = new CliOption("-a"); ValueSource valueSource = option; - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); - - success.Should() - .BeTrue(); - value.Should() - .Be(42); + if (valueSource.TryGetTypedValue(EmptyPipelineResult("-a 42", option), out var value)) + { + value.Should() + .Be(42); + return; + } + Assert.Fail("Typed value not retrieved"); } [Fact] @@ -136,12 +144,13 @@ public void RelativeToSymbolValueSource_from_extension_produces_value_that_was_s var option = new CliOption("-a"); var valueSource = new RelativeToSymbolValueSource(option); - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult("-a 42", option)); - - success.Should() - .BeTrue(); - value.Should() - .Be(42); + if (valueSource.TryGetTypedValue(EmptyPipelineResult("-a 42", option), out var value)) + { + value.Should() + .Be(42); + return; + } + Assert.Fail("Typed value not retrieved"); } [Fact] @@ -149,17 +158,18 @@ public void RelativeToEnvironmentVariableValueSource_produces_value_that_was_set { var envName = "SYSTEM_COMMANDLINE_TESTING"; var valueSource = new RelativeToEnvironmentVariableValueSource(envName); - + Environment.SetEnvironmentVariable(envName, "42"); - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult("")); + if (valueSource.TryGetTypedValue(EmptyPipelineResult(), out var value)) + { + value.Should() + .Be(42); + return; + } Environment.SetEnvironmentVariable(envName, null); - - success.Should() - .BeTrue(); - value.Should() - .Be(42); + Assert.Fail("Typed value not retrieved"); } - + [Fact] public void RelativeToEnvironmentVariableValueSource_from_extension_produces_value_that_was_set() @@ -168,12 +178,13 @@ public void RelativeToEnvironmentVariableValueSource_from_extension_produces_val var valueSource = ValueSource.CreateFromEnvironmentVariable(envName); Environment.SetEnvironmentVariable(envName, "42"); - (bool success, int value) = valueSource.GetTypedValue(EmptyPipelineResult("")); + if (valueSource.TryGetTypedValue(EmptyPipelineResult(), out var value)) + { + value.Should() + .Be(42); + return; + } Environment.SetEnvironmentVariable(envName, null); - - success.Should() - .BeTrue(); - value.Should() - .Be(42); + Assert.Fail("Typed value not retrieved"); } } diff --git a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs index 6334f8543f..fc0ac8cf18 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs @@ -33,22 +33,19 @@ public void Validate(object? value, if (comparableValue is null) return; // nothing to do // TODO: Replace the strings we are comparing with a diagnostic ID when we update ParseError - if (LowerBound is not null) + if (LowerBound is not null + && LowerBound.TryGetTypedValue(validationContext.PipelineResult, out var lowerValue) + && comparableValue.CompareTo(lowerValue) < 0) { - var lower = LowerBound.GetTypedValue(validationContext.PipelineResult); - if (lower.success && comparableValue.CompareTo(lower.value) < 0) - { - validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is below the lower bound of {LowerBound}")); - } + validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is below the lower bound of {LowerBound}")); } - if (UpperBound is not null) + + if (UpperBound is not null + && UpperBound.TryGetTypedValue(validationContext.PipelineResult, out var upperValue) + && comparableValue.CompareTo(upperValue) > 0) { - var upper = UpperBound.GetTypedValue(validationContext.PipelineResult); - if (upper.success && comparableValue.CompareTo(upper.value) > 0) - { - validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {UpperBound}")); - } + validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {UpperBound}")); } } diff --git a/src/System.CommandLine.Subsystems/ValueProvider.cs b/src/System.CommandLine.Subsystems/ValueProvider.cs index 50043cdeee..dd85d40046 100644 --- a/src/System.CommandLine.Subsystems/ValueProvider.cs +++ b/src/System.CommandLine.Subsystems/ValueProvider.cs @@ -51,12 +51,11 @@ private bool TryGetValue(CliSymbol symbol, out T? value) } if (valueSymbol.TryGetDefaultValueSource(out ValueSource? defaultValueSource)) { - if (defaultValueSource is not ValueSource typedValueSource) + if (defaultValueSource is not ValueSource typedDefaultValueSource) { throw new InvalidOperationException("Unexpected ValueSource type"); } - (var success, var defaultValue) = typedValueSource.GetTypedValue(pipelineResult); - if (success) + if( typedDefaultValueSource.TryGetTypedValue(pipelineResult, out T? defaultValue)) { return UseValue(valueSymbol, defaultValue); } diff --git a/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs index f3aff61d88..10286bed59 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs @@ -21,25 +21,25 @@ public AggregateValueSource(ValueSource firstSource, public bool PrecedenceAsEntered { get; set; } - public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) - => ValueFromSources(pipelineResult); - - private (bool success, T? value) ValueFromSources(PipelineResult pipelineResult) + public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) { var orderedSources = PrecedenceAsEntered ? valueSources : [.. valueSources.OrderBy(GetPrecedence)]; foreach (var source in orderedSources) { - (var success, var value) = source.GetTypedValue(pipelineResult); - if (success) + if (source.TryGetTypedValue(pipelineResult, out var newValue)) { - return (true, value); + value = newValue; + return true; } } - return (false, default); + value = default; + return false; + } + // TODO: Discuss precedence vs order entered for aggregates internal static int GetPrecedence(ValueSource source) { return source switch diff --git a/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs index 6cf25b4c28..12fb78a666 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs @@ -9,7 +9,16 @@ public class CalculatedValueSource(Func<(bool success, T? value)> calculation { public override string? Description { get; } = description; - public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) - => calculation(); + public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) + { + (bool success, T? newValue) = calculation(); + if (success) + { + value = newValue; + return true; + } + value = default; + return false; + } } diff --git a/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs index 132504df94..5ac9d649cb 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs @@ -10,25 +10,41 @@ public class RelativeToEnvironmentVariableValueSource(string environmentVaria { public override string? Description { get; } = description; - public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) + public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) { string? stringValue = Environment.GetEnvironmentVariable(environmentVariableName); if (stringValue is null) { - // This feels wrong. It isn't saying "Hey, you asked for a value that was not there" - return default; + value = default; + return false; } - // TODO: What is the best way to do this? - T value = default(T) switch + // TODO: Unify this with System.CommandLine.ArgumentConverter conversions, which will require changes to that code. + // This will provide consistency, including support for nullable value types, and custom type conversions + try { - int i => (T)(object)Convert.ToInt32(stringValue), - _ => throw new NotImplementedException("Looking for a non-dumb way to do this") - }; - return calculation is null - ? (true, value) - : calculation(Environment.GetEnvironmentVariable(environmentVariableName)); + if (calculation is not null) + { + (var success, var calcValue) = calculation(stringValue); + if (success) + { + value = calcValue; + return true; + } + value = default; + return false; + } + var newValue = Convert.ChangeType(stringValue, typeof(T)); + value = (T?)newValue; + return true; + } + catch + { + // TODO: This probably represents a failure converting from string, so in user's world to fix. How do we report this? + value = default; + return false; + } } } diff --git a/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs index 9595a6eb75..beb11805f2 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs @@ -4,15 +4,36 @@ namespace System.CommandLine.ValueSources; public class RelativeToSymbolValueSource(CliValueSymbol otherSymbol, - Func? calculation = null, + bool onlyUserEnteredValues = false, + Func? calculation = null, string? description = null) : ValueSource { public override string? Description { get; } = description; - public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) - => calculation is null - ? (true, pipelineResult.GetValue(otherSymbol)) - : calculation(pipelineResult.GetValue(otherSymbol)); + public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) + { + if (onlyUserEnteredValues && pipelineResult.GetValueResult(otherSymbol) is null) + { + value = default; + return false; + } + + var otherSymbolValue = pipelineResult.GetValue(otherSymbol); + + if (calculation is null) + { + value = otherSymbolValue; + return true; + } + (var success, var newValue) = calculation(otherSymbolValue); + if (success) + { + value = newValue; + return true; + } + value = default; + return false; + } } diff --git a/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs index a4fcbae3d4..cbee6e7539 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs @@ -6,9 +6,13 @@ namespace System.CommandLine.ValueSources; public class SimpleValueSource(T value, string? description = null) : ValueSource { + public T Value { get; } = value; public override string? Description { get; } = description; - public override (bool success, T? value) GetTypedValue(PipelineResult pipelineResult) - => (true, value); + public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) + { + value = Value; + return true; + } } diff --git a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs index cd2f547556..38b67c1d90 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs @@ -5,35 +5,49 @@ namespace System.CommandLine.ValueSources; public abstract class ValueSource { - public abstract (bool success, object? value) GetValue(PipelineResult pipelineResult); + public abstract bool TryGetValue(PipelineResult pipelineResult, out object? value); // TODO: Should we use ToString() here? public abstract string? Description { get; } public static ValueSource Create(T value, string? description = null) => new SimpleValueSource(value, description); - public static ValueSource Create(Func<(bool success, T? value)> calculation, string? description = null) - => new CalculatedValueSource(calculation); - - public static ValueSource Create(CliValueSymbol otherSymbol, Func? calculation = null, string? description = null) - => new RelativeToSymbolValueSource(otherSymbol, calculation, description); - - public static ValueSource Create(ValueSource firstSource, ValueSource secondSource, string? description = null, params ValueSource[] otherSources) - { - return new AggregateValueSource(firstSource, secondSource, description, otherSources); - } - - public static ValueSource CreateFromEnvironmentVariable(string environmentVariableName, Func? calculation = null, string? description = null) + public static ValueSource Create(Func<(bool success, T? value)> calculation, + string? description = null) + => new CalculatedValueSource(calculation, description); + + public static ValueSource Create(CliValueSymbol otherSymbol, + Func? calculation = null, + bool userEnteredValueOnly = false, + string? description = null) + => new RelativeToSymbolValueSource(otherSymbol, userEnteredValueOnly, calculation, description); + + public static ValueSource Create(ValueSource firstSource, + ValueSource secondSource, + string? description = null, + params ValueSource[] otherSources) + => new AggregateValueSource(firstSource, secondSource, description, otherSources); + + public static ValueSource CreateFromEnvironmentVariable(string environmentVariableName, + Func? calculation = null, + string? description = null) => new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description); } public abstract class ValueSource : ValueSource { - public abstract (bool success, T? value) GetTypedValue(PipelineResult pipelineResult); + public abstract bool TryGetTypedValue(PipelineResult pipelineResult, out T? value); - public override (bool success, object? value) GetValue(PipelineResult pipelineResult) + public override bool TryGetValue(PipelineResult pipelineResult, out object? value) { - return GetTypedValue(pipelineResult); + + if (TryGetTypedValue(pipelineResult, out T? newValue)) + { + value = newValue; + return true; + } + value = null; + return false; } public static implicit operator ValueSource(T value) => new SimpleValueSource(value); From b2c13b7ca3f64e15ff7e7407e19b38f3d4c1dccb Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Tue, 27 Aug 2024 12:27:32 -0400 Subject: [PATCH 139/150] Rolled back ValueAnnotationExtensions. NotNullWhen to ValueSource.TryGet... --- .../ValueAnnotationExtensions.cs | 154 +++++++++++++++--- .../ValueSources/ValueSource.cs | 4 +- 2 files changed, 136 insertions(+), 22 deletions(-) diff --git a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs index 6b00ed3bec..676bcc9b7d 100644 --- a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs @@ -10,7 +10,6 @@ namespace System.CommandLine; public static class ValueAnnotationExtensions { - /// /// Get the default value annotation for the /// @@ -34,6 +33,31 @@ public static bool TryGetDefaultValueSource(this CliValueSymbol valueSymbol, [No public static void SetDefaultValueSource(this CliValueSymbol valueSymbol, ValueSource defaultValue) => valueSymbol.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); + + /// + /// Sets the default value annotation on the + /// + /// The type of the option value + /// The option + /// The default value for the option + /// The , to enable fluent construction of symbols with annotations. + public static CliOption WithDefaultValue(this CliOption option, TValue defaultValue) + { + option.SetDefaultValue(defaultValue); + return option; + } + + /// + /// Sets the default value annotation on the + /// + /// The type of the option value + /// The option + /// The default value for the option + public static void SetDefaultValue(this CliOption option, TValue defaultValue) + { + option.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); + } + /// /// Get the default value annotation for the /// @@ -45,17 +69,27 @@ public static void SetDefaultValueSource(this CliValueSymbol valueSymbol, ValueS /// which calculates the actual default value, based on the default value annotation and default value calculation, /// whether directly stored on the symbol or from the subsystem's . /// - public static bool TryGetDefaultValueAnnotation(this CliValueSymbol valueSymbol, out TValue? defaultValue) - => valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValue, out defaultValue); + public static TValue? GetDefaultValueAnnotation(this CliOption option) + { + if (option.TryGetAnnotation(ValueAnnotations.DefaultValue, out TValue? defaultValue)) + { + return defaultValue; + } + return default; + } /// - /// Sets the default value annotation on the + /// Sets the default value annotation on the /// - /// The type of the option value - /// The option - /// The default value for the option - public static void SetDefaultValue(this CliOption option, TValue defaultValue) - => option.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); + /// The type of the argument value + /// The argument + /// The default value for the argument + /// The , to enable fluent construction of symbols with annotations. + public static CliArgument WithDefaultValue(this CliArgument argument, TValue defaultValue) + { + argument.SetDefaultValue(defaultValue); + return argument; + } /// /// Sets the default value annotation on the @@ -64,22 +98,43 @@ public static void SetDefaultValue(this CliOption option, TValue /// The argument /// The default value for the argument /// The , to enable fluent construction of symbols with annotations. - public static void SetDefaultValue(this CliArgument argument, TValue defaultValue) - => argument.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); + public static void SetDefaultValue(this CliArgument argument, TValue defaultValue) + { + argument.SetAnnotation(ValueAnnotations.DefaultValue, defaultValue); + } /// - /// Get the default value calculation for the + /// Get the default value annotation for the /// - /// The type of the option value - /// The option - /// The option's default value calculation if any, otherwise + /// The type of the argument value + /// The argument + /// The argument's default value annotation if any, otherwise /// - /// This is intended to be called by CLI authors. Subsystems should instead call , + /// This is intended to be called by CLI authors. Subsystems should instead call , /// which calculates the actual default value, based on the default value annotation and default value calculation, /// whether directly stored on the symbol or from the subsystem's . /// - public static bool TryGetDefaultValueCalculation(this CliValueSymbol valueSymbol, out Func? calculation) - => valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out calculation); + public static TValue? GetDefaultValueAnnotation(this CliArgument argument) + { + if (argument.TryGetAnnotation(ValueAnnotations.DefaultValue, out TValue? defaultValue)) + { + return (TValue?)defaultValue; + } + return default; + } + + /// + /// Sets the default value calculation for the + /// + /// The type of the option value + /// The option + /// The default value calculation for the option + /// The , to enable fluent construction of symbols with annotations. + public static CliOption WithDefaultValueCalculation(this CliOption option, Func defaultValueCalculation) + { + option.SetDefaultValueCalculation(defaultValueCalculation); + return option; + } /// /// Sets the default value calculation for the @@ -88,7 +143,29 @@ public static bool TryGetDefaultValueCalculation(this CliValueSymbol va /// The option /// The default value calculation for the option public static void SetDefaultValueCalculation(this CliOption option, Func defaultValueCalculation) - => option.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); + { + option.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); + } + + /// + /// Get the default value calculation for the + /// + /// The type of the option value + /// The option + /// The option's default value calculation if any, otherwise + /// + /// This is intended to be called by CLI authors. Subsystems should instead call , + /// which calculates the actual default value, based on the default value annotation and default value calculation, + /// whether directly stored on the symbol or from the subsystem's . + /// + public static Func? GetDefaultValueCalculation(this CliOption option) + { + if (option.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation)) + { + return defaultValueCalculation; + } + return default; + } /// /// Sets the default value calculation for the @@ -97,6 +174,41 @@ public static void SetDefaultValueCalculation(this CliOption opt /// The argument /// The default value calculation for the argument /// The , to enable fluent construction of symbols with annotations. - public static void SetDefaultValueCalculation(this CliArgument argument, Func defaultValueCalculation) - => argument.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); + public static CliArgument WithDefaultValueCalculation(this CliArgument argument, Func defaultValueCalculation) + { + argument.SetDefaultValueCalculation(defaultValueCalculation); + return argument; + } + + /// + /// Sets the default value calculation for the + /// + /// The type of the argument value + /// The argument + /// The default value calculation for the argument + /// The , to enable fluent construction of symbols with annotations. + public static void SetDefaultValueCalculation(this CliArgument argument, Func defaultValueCalculation) + { + argument.SetAnnotation(ValueAnnotations.DefaultValueCalculation, defaultValueCalculation); + } + + /// + /// Get the default value calculation for the + /// + /// The type of the argument value + /// The argument + /// The argument's default value calculation if any, otherwise + /// + /// This is intended to be called by CLI authors. Subsystems should instead call , + /// which calculates the actual default value, based on the default value annotation and default value calculation, + /// whether directly stored on the symbol or from the subsystem's . + /// + public static Func? GetDefaultValueCalculation(this CliArgument argument) + { + if (argument.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation)) + { + return defaultValueCalculation; + } + return default; + } } diff --git a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs index 38b67c1d90..1470d628db 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs @@ -1,6 +1,8 @@ // 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.CodeAnalysis; + namespace System.CommandLine.ValueSources; public abstract class ValueSource @@ -38,7 +40,7 @@ public abstract class ValueSource : ValueSource { public abstract bool TryGetTypedValue(PipelineResult pipelineResult, out T? value); - public override bool TryGetValue(PipelineResult pipelineResult, out object? value) + public override bool TryGetValue(PipelineResult pipelineResult, [NotNullWhen(true)]out object? value) { if (TryGetTypedValue(pipelineResult, out T? newValue)) From f14d7d02d05b68fdaf0c4074bed2bf7ecfb773ab Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Fri, 30 Aug 2024 07:55:23 -0400 Subject: [PATCH 140/150] Respond to review --- .../Annotations/ValueAnnotations.cs | 6 +-- .../Annotations/ValueConditionAnnotations.cs | 2 +- .../Subsystems/CliSubsystem.cs | 1 + .../Validation/CommandValidator.cs | 9 +++++ .../Validation/ICommandValidator.cs | 11 +++++- .../Validation/IValueValidator.cs | 13 +++++++ .../Validation/InclusiveGroupValidator.cs | 4 ++ .../Validation/RangeValidator.cs | 4 ++ .../Validation/ValidationContext.cs | 2 + .../Validation/Validator.cs | 4 ++ .../Validation/ValueValidator.cs | 12 ++++++ .../ValueConditions/InclusiveGroup.cs | 11 ++++++ .../ValueConditions/Range.cs | 3 ++ .../ValueConditions/RangeBounds.cs | 26 +++++++++++++ .../ValueProvider.cs | 25 +----------- .../ValueSources/AggregateValueSource.cs | 8 ++-- .../ValueSources/CalculatedValueSource.cs | 13 +++++-- ...elativeToEnvironmentVariableValueSource.cs | 36 +++++++++++++---- .../RelativeToSymbolValueSource.cs | 39 ++++++++++++++----- .../ValueSources/SimpleValueSource.cs | 12 ++++-- .../ValueSources/ValueSource.cs | 25 +++++++++++- 21 files changed, 210 insertions(+), 56 deletions(-) diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs index 0e8088cb64..7695c363e0 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueAnnotations.cs @@ -4,7 +4,7 @@ namespace System.CommandLine.Subsystems.Annotations; /// -/// IDs for well-known Version annotations. +/// IDs for well-known Default Value annotations. /// public static class ValueAnnotations { @@ -16,7 +16,7 @@ public static class ValueAnnotations public static AnnotationId DefaultValueSource { get; } = new(Prefix, nameof(DefaultValueSource)); /// - /// Default value for an option or argument + /// Default default value for an option or argument /// /// /// Should be the same type as the type parameter of @@ -26,7 +26,7 @@ public static class ValueAnnotations /// - /// Default value calculation for an option or argument + /// Default default value calculation for an option or argument /// /// /// Please use the extension methods and do not call this directly. diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs index d89efc2b8b..574c111d2a 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/ValueConditionAnnotations.cs @@ -4,7 +4,7 @@ namespace System.CommandLine.Subsystems.Annotations; /// -/// IDs for well-known Version annotations. +/// IDs for well-known Value Condition annotations. /// public static class ValueConditionAnnotations { diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index d02f0e606a..49e6f98af7 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -108,6 +108,7 @@ protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId annotati /// /// The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate /// A PipelineResult object with information such as whether the CLI should terminate + // These methods are public to support use of subsystems without the pipeline public virtual void Execute(PipelineResult pipelineResult) => pipelineResult.NotRun(pipelineResult.ParseResult); diff --git a/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs b/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs index c2aadd25e9..df58f9e655 100644 --- a/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/CommandValidator.cs @@ -5,11 +5,20 @@ namespace System.CommandLine.Validation; +/// +/// Base class for validators that affect the entire command +/// public abstract class CommandValidator : Validator { protected CommandValidator(string name, Type valueConditionType, params Type[] moreValueConditionTypes) : base(name, valueConditionType, moreValueConditionTypes) { } + /// + /// Validation method specific to command results. + /// + /// The + /// The + /// The public abstract void Validate(CliCommandResult commandResult, CommandCondition commandCondition, ValidationContext validationContext); } diff --git a/src/System.CommandLine.Subsystems/Validation/ICommandValidator.cs b/src/System.CommandLine.Subsystems/Validation/ICommandValidator.cs index 2286fde581..ff109370be 100644 --- a/src/System.CommandLine.Subsystems/Validation/ICommandValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/ICommandValidator.cs @@ -5,9 +5,18 @@ namespace System.CommandLine.Validation; +/// +/// Interface that allows non-Validator derived methods to perform validation. Specifically, this supports +/// instances that can validate. +/// public interface ICommandValidator { - + /// + /// Validation method specific to command results + /// + /// The + /// The + /// The void Validate(CliCommandResult commandResult, CommandCondition commandCondition, ValidationContext validationContext); } diff --git a/src/System.CommandLine.Subsystems/Validation/IValueValidator.cs b/src/System.CommandLine.Subsystems/Validation/IValueValidator.cs index 2747fb78c1..bca36f53c3 100644 --- a/src/System.CommandLine.Subsystems/Validation/IValueValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/IValueValidator.cs @@ -5,8 +5,21 @@ namespace System.CommandLine.Validation; +/// +/// Interface that allows non-Validator derived methods to perform validation. Specifically, this supports +/// instances that can validate. +/// public interface IValueValidator { + // Note: We pass both valueSymbol and valueResult, because we may validate symbols where valueResult is null. + /// + /// Validation method specific to value results. + /// + /// The value to validate. + /// The of the value to validate. + /// The of the value to validate. + /// The + /// The void Validate(object? value, CliValueSymbol valueSymbol, CliValueResult? valueResult, ValueCondition valueCondition, ValidationContext validationContext); } diff --git a/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs b/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs index c478be5a98..cadb1759a1 100644 --- a/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs @@ -6,11 +6,15 @@ namespace System.CommandLine.Validation; +/// +/// Validator that requires that if one member of the group is present, they are all present. +/// public class InclusiveGroupValidator : CommandValidator { public InclusiveGroupValidator() : base(nameof(InclusiveGroup), typeof(InclusiveGroup)) { } + /// public override void Validate(CliCommandResult commandResult, CommandCondition valueCondition, ValidationContext validationContext) { diff --git a/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs b/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs index e75429156c..2c9d6ec4d5 100644 --- a/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs @@ -5,11 +5,15 @@ namespace System.CommandLine.Validation; +/// +/// Validates that a value is within the specified bounds. +/// public class RangeValidator : ValueValidator, IValueValidator { public RangeValidator() : base(nameof(ValueConditions.Range), typeof(ValueConditions.Range)) { } + /// public override void Validate(object? value, CliValueSymbol valueSymbol, CliValueResult? valueResult, ValueCondition valueCondition, ValidationContext validationContext) { diff --git a/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs b/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs index bd444e7552..cdd7e184d7 100644 --- a/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs +++ b/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs @@ -3,6 +3,8 @@ namespace System.CommandLine.Validation; +// TODO: Remove this class. All of the things it contains are in the PipelineResult, except the ValidationSubsystem currently +// running, if there are multiple. The scenario where that is needed seems unlikely. public class ValidationContext { public ValidationContext(PipelineResult pipelineResult, ValidationSubsystem validationSubsystem) diff --git a/src/System.CommandLine.Subsystems/Validation/Validator.cs b/src/System.CommandLine.Subsystems/Validation/Validator.cs index 11eb556e6f..09bf714455 100644 --- a/src/System.CommandLine.Subsystems/Validation/Validator.cs +++ b/src/System.CommandLine.Subsystems/Validation/Validator.cs @@ -4,6 +4,10 @@ namespace System.CommandLine.Validation; +// TODO: This may be removed if we settle on ValueCondition validation only. +/// +/// Base class for CommandValidator and ValueValidator. +/// public abstract class Validator { public Validator(string name, Type valueConditionType, params Type[] moreValueConditionTypes) diff --git a/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs b/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs index 7029dd37a3..39685a015c 100644 --- a/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/ValueValidator.cs @@ -5,6 +5,9 @@ namespace System.CommandLine.Validation; +/// +/// Base class for validators that affect a single symbol. +/// public abstract class ValueValidator : Validator { protected ValueValidator(string name, Type valueConditionType, params Type[] moreValueConditionTypes) @@ -16,6 +19,15 @@ protected TValue GetValueAsTypeOrThrow(object? value) ? typedValue : throw new InvalidOperationException($"{Name} validation does not apply to this type"); + /// + /// Validation method specific to a single symbols value.results + /// + /// The value to validate. + /// The option or argument being validated. + /// The + /// The + /// The public abstract void Validate(object? value, CliValueSymbol valueSymbol, CliValueResult? valueResult, ValueCondition valueCondition, ValidationContext validationContext); } + diff --git a/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs b/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs index f16c0e851c..29628a12fe 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/InclusiveGroup.cs @@ -3,15 +3,26 @@ namespace System.CommandLine.ValueConditions; +/// +/// Describes that a set of options and arguments must all be entered +/// if one or more are entered. +/// public class InclusiveGroup : CommandCondition { private IEnumerable group = []; + /// + /// The constructor for InclusiveGroup. + /// + /// The group of options and arguments that must all be present, or note be present. public InclusiveGroup(IEnumerable group) : base(nameof(InclusiveGroup)) { this.group = group; } + /// + /// The members of the inclusive group. + /// public IEnumerable Members => group.ToList(); } diff --git a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs index fc0ac8cf18..e495fd7db7 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs @@ -7,6 +7,9 @@ namespace System.CommandLine.ValueConditions; +/// +/// Declares the range for the option or argument. The non-generic version is used by the (valueSymbol, out var value) - // => value, // It has already been retrieved at least once - // { } when parseResult?.GetValueResult(valueSymbol) is { } valueResult // GetValue not used because it would always return a value - // => UseValue(valueSymbol, valueResult.GetValue()), // Value was supplied during parsing, - // // Value was not supplied during parsing, determine default now - // // configuration values go here in precedence - // //not null when GetDefaultFromEnvironmentVariable(symbol, out var envName) - // // => UseValue(symbol, GetEnvByName(envName)), - // { } when valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValueCalculation, out Func? defaultValueCalculation) - // => UseValue(valueSymbol, CalculatedDefault(valueSymbol, (Func)defaultValueCalculation)), - // { } when valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValue, out T? explicitValue) - // => UseValue(valueSymbol, explicitValue), - // null => throw new ArgumentNullException(nameof(valueSymbol)), - // _ => UseValue(valueSymbol, default(T)) - //}; - TValue UseValue(CliSymbol symbol, TValue value) { SetValue(symbol, value); diff --git a/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs index 10286bed59..751b789794 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/AggregateValueSource.cs @@ -3,11 +3,11 @@ namespace System.CommandLine.ValueSources; -public class AggregateValueSource : ValueSource +public sealed class AggregateValueSource : ValueSource { private List> valueSources = []; - public AggregateValueSource(ValueSource firstSource, + internal AggregateValueSource(ValueSource firstSource, ValueSource secondSource, string? description = null, params ValueSource[] otherSources) @@ -45,8 +45,8 @@ internal static int GetPrecedence(ValueSource source) return source switch { SimpleValueSource => 0, - CalculatedValueSource => 1, - RelativeToSymbolValueSource => 2, + RelativeToSymbolValueSource => 1, + CalculatedValueSource => 2, //RelativeToConfigurationValueSource => 3, RelativeToEnvironmentVariableValueSource => 4, _ => 5 diff --git a/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs index 12fb78a666..18b3e7d04e 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/CalculatedValueSource.cs @@ -4,10 +4,17 @@ namespace System.CommandLine.ValueSources; // Find an example of when this is useful beyond Random and Guid. Is a time lag between building the CLI and validating important (DateTime.Now()) -public class CalculatedValueSource(Func<(bool success, T? value)> calculation, string? description = null) - : ValueSource +public sealed class CalculatedValueSource : ValueSource { - public override string? Description { get; } = description; + private readonly Func<(bool success, T? value)> calculation; + + internal CalculatedValueSource(Func<(bool success, T? value)> calculation, string? description = null) + { + this.calculation = calculation; + Description = description; + } + + public override string? Description { get; } public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) { diff --git a/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs index 5ac9d649cb..f2e69b0913 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs @@ -3,16 +3,38 @@ namespace System.CommandLine.ValueSources; -public class RelativeToEnvironmentVariableValueSource(string environmentVariableName, - Func? calculation = null, - string? description = null) +/// +/// that returns the converted value of the specified environment variable. +/// If the calculation delegate is supplied, the returned value of the calculation is returned. +/// +/// The type to be returned, which is almost always the type of the symbol the ValueSource will be used for. +/// The name of then environment variable. Note that for some systems, this is case sensitive. +/// A delegate that returns the requested type. If it is not specified, standard type conversions are used. +/// The description of this value, used to clarify the intent of the values that appear in error messages. +public sealed class RelativeToEnvironmentVariableValueSource : ValueSource { - public override string? Description { get; } = description; + internal RelativeToEnvironmentVariableValueSource( + string environmentVariableName, + Func? calculation = null, + string? description = null) + { + EnvironmentVariableName = environmentVariableName; + Calculation = calculation; + Description = description; + } + + public string EnvironmentVariableName { get; } + public Func? Calculation { get; } + + /// + /// The description of this value, used to clarify the intent of the values that appear in error messages. + /// + public override string? Description { get; } public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) { - string? stringValue = Environment.GetEnvironmentVariable(environmentVariableName); + string? stringValue = Environment.GetEnvironmentVariable(EnvironmentVariableName); if (stringValue is null) { @@ -24,9 +46,9 @@ public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? valu // This will provide consistency, including support for nullable value types, and custom type conversions try { - if (calculation is not null) + if (Calculation is not null) { - (var success, var calcValue) = calculation(stringValue); + (var success, var calcValue) = Calculation(stringValue); if (success) { value = calcValue; diff --git a/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs index beb11805f2..a53e13c573 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs @@ -3,30 +3,51 @@ namespace System.CommandLine.ValueSources; -public class RelativeToSymbolValueSource(CliValueSymbol otherSymbol, - bool onlyUserEnteredValues = false, - Func? calculation = null, - string? description = null) +/// +/// that returns the value of the specified other symbol. +/// If the calculation delegate is supplied, the returned value of the calculation is returned. +/// +/// The type to be returned, which is almost always the type of the symbol the ValueSource will be used for. +/// The option or argument to return, with the calculation supplied if it is not null. +/// A delegate that returns the requested type. +/// The description of this value, used to clarify the intent of the values that appear in error messages. +public sealed class RelativeToSymbolValueSource : ValueSource { - public override string? Description { get; } = description; + internal RelativeToSymbolValueSource( + CliValueSymbol otherSymbol, + bool onlyUserEnteredValues = false, + Func? calculation = null, + string? description = null) + { + OtherSymbol = otherSymbol; + OnlyUserEnteredValues = onlyUserEnteredValues; + Calculation = calculation; + Description = description; + } + + public override string? Description { get; } + public CliValueSymbol OtherSymbol { get; } + public bool OnlyUserEnteredValues { get; } + public Func? Calculation { get; } + /// public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) { - if (onlyUserEnteredValues && pipelineResult.GetValueResult(otherSymbol) is null) + if (OnlyUserEnteredValues && pipelineResult.GetValueResult(OtherSymbol) is null) { value = default; return false; } - var otherSymbolValue = pipelineResult.GetValue(otherSymbol); + var otherSymbolValue = pipelineResult.GetValue(OtherSymbol); - if (calculation is null) + if (Calculation is null) { value = otherSymbolValue; return true; } - (var success, var newValue) = calculation(otherSymbolValue); + (var success, var newValue) = Calculation(otherSymbolValue); if (success) { value = newValue; diff --git a/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs index cbee6e7539..6732f8268e 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/SimpleValueSource.cs @@ -3,11 +3,17 @@ namespace System.CommandLine.ValueSources; -public class SimpleValueSource(T value, string? description = null) +public sealed class SimpleValueSource : ValueSource { - public T Value { get; } = value; - public override string? Description { get; } = description; + internal SimpleValueSource(T value, string? description = null) + { + Value = value; + Description = description; + } + + public T Value { get; } + public override string? Description { get; } public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) { diff --git a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs index 1470d628db..a36b71589c 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs @@ -7,6 +7,17 @@ namespace System.CommandLine.ValueSources; public abstract class ValueSource { + internal ValueSource() + { + + } + + /// + /// Supplies the requested value, with the calculation applied if it is not null. + /// + /// The current pipeline result. + /// An out parameter which contains the converted value, with the calculation applied, if it is found. + /// True if a value was found, otherwise false. public abstract bool TryGetValue(PipelineResult pipelineResult, out object? value); // TODO: Should we use ToString() here? @@ -36,11 +47,21 @@ public static ValueSource CreateFromEnvironmentVariable(string environment => new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description); } +// TODO: Determine philosophy for custom value sources and whether tehy can buld on existing sources. public abstract class ValueSource : ValueSource { - public abstract bool TryGetTypedValue(PipelineResult pipelineResult, out T? value); + /// + /// Supplies the requested value, with the calculation applied if it is not null. + /// + /// The current pipeline result. + /// An out parameter which contains the converted value, with the calculation applied, if it is found. + /// True if a value was found, otherwise false. + public abstract bool TryGetTypedValue(PipelineResult pipelineResult, + [NotNullWhen(true)] out T? value); - public override bool TryGetValue(PipelineResult pipelineResult, [NotNullWhen(true)]out object? value) + /// + public override bool TryGetValue(PipelineResult pipelineResult, + [NotNullWhen(true)] out object? value) { if (TryGetTypedValue(pipelineResult, out T? newValue)) From 7aff35a358576f11f4a646959c3d7c4f613a34eb Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Fri, 30 Aug 2024 08:00:10 -0400 Subject: [PATCH 141/150] Respond to review --- .../Validation/InclusiveGroupValidator.cs | 1 + src/System.CommandLine.Subsystems/Validation/Validator.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs b/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs index cadb1759a1..3741f98fa7 100644 --- a/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs @@ -41,6 +41,7 @@ public override void Validate(CliCommandResult commandResult, } if (missingMembers is not null && missingMembers.Any()) { + // TODO: Rework to allow localization var pluralToBe = "are"; var singularToBe = "is"; validationContext.PipelineResult.AddError(new ParseError( $"The members {string.Join(", ", groupMembers.Select(m => m.Name))} " + diff --git a/src/System.CommandLine.Subsystems/Validation/Validator.cs b/src/System.CommandLine.Subsystems/Validation/Validator.cs index 09bf714455..4fded1927d 100644 --- a/src/System.CommandLine.Subsystems/Validation/Validator.cs +++ b/src/System.CommandLine.Subsystems/Validation/Validator.cs @@ -8,6 +8,7 @@ namespace System.CommandLine.Validation; /// /// Base class for CommandValidator and ValueValidator. /// +// TODO: Discuss visibility and custom validators public abstract class Validator { public Validator(string name, Type valueConditionType, params Type[] moreValueConditionTypes) From bd98d30cad3922fac0867d438d3b389fdb654249 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 31 Aug 2024 06:46:05 -0400 Subject: [PATCH 142/150] Cleanup, notes, and a small fix --- OpenQuestions.md | 56 +++++++++++++++++++ System.CommandLine.sln | 1 + .../AlternateSubsystems.cs | 4 +- .../System.CommandLine.Subsystems.csproj | 6 ++ ...elativeToEnvironmentVariableValueSource.cs | 2 +- .../ValueSources/ValueSource.cs | 12 ++-- .../System.CommandLine.csproj | 1 + 7 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 OpenQuestions.md diff --git a/OpenQuestions.md b/OpenQuestions.md new file mode 100644 index 0000000000..2e0d2fd308 --- /dev/null +++ b/OpenQuestions.md @@ -0,0 +1,56 @@ +# Open questions + +Please also include TODO in appropriate locations. This is intended as a wrap up. + +Also, include whether the question is to add or remove something, and add date/initials + +## NotNulWhen on TryGetValue in ValueSource and ValueProvider + +Things to consider: + +* Within System.CommandLine all TryGetValue should probably be the same +* TryGetValue on dictionary can return null +* For nullable values, the actual value can be null +* For nullable or non-nullable ref types, the default for the type is null +* Allowing null out values keeps a single meaning to "not found" and allows "found but null". Conflating these blocks expressing which happened + +The recovery is the same as with Dictionary.TryGetValue. The first line of the block that handles the return Boolean is a guard. + +## The extensibility story for ValueSource + +The proposal and current code seal our value sources and expect people to make additional ones based on ValueSource. The classes are public and sealed, the constructors are internal. + +Reasons to reconsider: Aggregate value source has a logical precedence or an as entered one. If someone adds a new value source, it is always last in the logic precedence.There are likely to be other similar cases. + +Possible resolution: Have this be case by case and allow aggregate values to be unsealed and have a mechanism for overriding. Providing a non-inheritance based solution could make this look like a normal operation when it is a rare one. + +## Contexts [RESOLVED] + +We had two different philosophies at different spots in subsystems. "Give folks everything they might need" and "Give folks only what we know they need". + +The first probably means we pass around `PipelineResult`. The second means that each purpose needs a special context. Sharing contexts is likely to mean that something will be added to one context that is unneeded by the other. Known expected contexts are: + +- `AnnotationProviderContext` +- `ValueSourceContext` +- `ValidationContext` (includes ability to report diagnostics) +- `CompletionContext` +- `HelpContext` + +## Which contexts should allow diagnostic reporting? + +## Should we have both Validators and IValidator on Conditions? [RESOLVED] + +We started with `Validators` and then added the IValidator interface to allow conditions to do validation because they have the strong type. Checking for this first also avoids a dictionary lookup. + +Our default validations will be on the Condition for the shortcut. Users can offer alternatives by creaing custom validators. The dictionary for custom validators will be lazy, and lookups will be pay for play when the user has custom validators. (This is not yet implemented.) + +When present, custom validators have precedence. There is no cost when they are not present. + +## Should conditions be public + +Since there are factory methods and validators could still access them, current behavior could be supported with internal conditions. + +However, the point of conditions is that they are a statement about the symbol and not an implementation. They are known to be used by completions and completions are expected to be extended. Thus, to get the values held in the condition (such as environment variable name) need to be available outside the external scope. + +Suggestion: Use internal constructors and leave conditions public + diff --git a/System.CommandLine.sln b/System.CommandLine.sln index ea10b088e6..b3d9488c91 100644 --- a/System.CommandLine.sln +++ b/System.CommandLine.sln @@ -16,6 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Packages.props = Directory.Packages.props global.json = global.json LICENSE.md = LICENSE.md + OpenQuestions.md = OpenQuestions.md README.md = README.md restore.cmd = restore.cmd restore.sh = restore.sh diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index 9745a61a90..5677470b03 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -43,7 +43,7 @@ internal class VersionWithInitializeAndTeardown : VersionSubsystem internal bool ExecutionWasRun; internal bool TeardownWasRun; - protected override void Initialize(InitializationContext context) + protected internal override void Initialize(InitializationContext context) { base.Initialize(context); // marker hack needed because ConsoleHack not available in initialization @@ -56,7 +56,7 @@ public override void Execute(PipelineResult pipelineResult) base.Execute(pipelineResult); } - protected override void TearDown(PipelineResult pipelineResult) + protected internal override void TearDown(PipelineResult pipelineResult) { TeardownWasRun = true; base.TearDown(pipelineResult); diff --git a/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj b/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj index 8416c9e173..2f5b0e4d91 100644 --- a/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj +++ b/src/System.CommandLine.Subsystems/System.CommandLine.Subsystems.csproj @@ -5,12 +5,18 @@ enable enable System.CommandLine + true + + + + + diff --git a/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs index f2e69b0913..07faef3ff4 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/RelativeToEnvironmentVariableValueSource.cs @@ -30,7 +30,7 @@ internal RelativeToEnvironmentVariableValueSource( /// /// The description of this value, used to clarify the intent of the values that appear in error messages. /// - public override string? Description { get; } + public override string? Description { get; } public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) { diff --git a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs index a36b71589c..43098896ed 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs @@ -22,6 +22,7 @@ internal ValueSource() // TODO: Should we use ToString() here? public abstract string? Description { get; } + public static ValueSource Create(T value, string? description = null) => new SimpleValueSource(value, description); @@ -47,7 +48,7 @@ public static ValueSource CreateFromEnvironmentVariable(string environment => new RelativeToEnvironmentVariableValueSource(environmentVariableName, calculation, description); } -// TODO: Determine philosophy for custom value sources and whether tehy can buld on existing sources. +// TODO: Determine philosophy for custom value sources and whether they can build on existing sources. public abstract class ValueSource : ValueSource { /// @@ -56,12 +57,13 @@ public abstract class ValueSource : ValueSource /// The current pipeline result. /// An out parameter which contains the converted value, with the calculation applied, if it is found. /// True if a value was found, otherwise false. - public abstract bool TryGetTypedValue(PipelineResult pipelineResult, - [NotNullWhen(true)] out T? value); + // TODO: Determine whether this and `TryGetValue` should have NotNullWhen(true) attribute. Discussion in /OpenQuestions.md + public abstract bool TryGetTypedValue(PipelineResult pipelineResult, + out T? value); /// - public override bool TryGetValue(PipelineResult pipelineResult, - [NotNullWhen(true)] out object? value) + public override bool TryGetValue(PipelineResult pipelineResult, + out object? value) { if (TryGetTypedValue(pipelineResult, out T? newValue)) diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 841860250d..cfb211a276 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -69,6 +69,7 @@ + From 20fc56f006f530929d734a0fe9005171b1fcb168 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 31 Aug 2024 08:59:43 -0400 Subject: [PATCH 143/150] Add XML docs, adjust scope, remove some unused stuff --- OpenQuestions.md | 3 + src/System.CommandLine.Subsystems/Pipeline.cs | 2 +- .../PipelineResult.cs | 8 +- .../Validation/InclusiveGroupValidator.cs | 2 +- .../Validation/RangeValidator.cs | 2 +- .../Validation/ValidationContext.cs | 53 +++++++++-- .../ValidationSubsystem.cs | 8 +- .../ValueCondition.cs | 14 +++ .../ValueConditionAnnotationExtensions.cs | 92 ++++++++++++++----- .../ValueConditions/Range.cs | 51 ++++++++-- .../ValueProvider.cs | 2 - 11 files changed, 182 insertions(+), 55 deletions(-) diff --git a/OpenQuestions.md b/OpenQuestions.md index 2e0d2fd308..9dd1770bd4 100644 --- a/OpenQuestions.md +++ b/OpenQuestions.md @@ -54,3 +54,6 @@ However, the point of conditions is that they are a statement about the symbol a Suggestion: Use internal constructors and leave conditions public +## Should `ValueCondition` be called `Condition`? + +They may apply to commands. \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 03b7842166..837fdea398 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -28,7 +28,7 @@ public partial class Pipeline /// A help subsystem to replace the standard one. To add a subsystem, use /// A new pipeline. /// - /// The , , , and cannot be replaced. + /// The , , and cannot be replaced. /// public static Pipeline Create(HelpSubsystem? help = null, VersionSubsystem? version = null, diff --git a/src/System.CommandLine.Subsystems/PipelineResult.cs b/src/System.CommandLine.Subsystems/PipelineResult.cs index e9972f1736..55b5f4963e 100644 --- a/src/System.CommandLine.Subsystems/PipelineResult.cs +++ b/src/System.CommandLine.Subsystems/PipelineResult.cs @@ -30,14 +30,14 @@ public PipelineResult(ParseResult parseResult, string rawInput, Pipeline? pipeli public bool AlreadyHandled { get; set; } public int ExitCode { get; set; } - public T? GetValue(CliValueSymbol dataSymbol) - => valueProvider.GetValue(dataSymbol); + public T? GetValue(CliValueSymbol valueSymbol) + => valueProvider.GetValue(valueSymbol); public object? GetValue(CliValueSymbol option) => valueProvider.GetValue(option); - public CliValueResult? GetValueResult(CliValueSymbol dataSymbol) - => ParseResult.GetValueResult(dataSymbol); + public CliValueResult? GetValueResult(CliValueSymbol valueSymbol) + => ParseResult.GetValueResult(valueSymbol); public void AddErrors(IEnumerable errors) diff --git a/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs b/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs index 3741f98fa7..6c8a1c28e5 100644 --- a/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/InclusiveGroupValidator.cs @@ -44,7 +44,7 @@ public override void Validate(CliCommandResult commandResult, // TODO: Rework to allow localization var pluralToBe = "are"; var singularToBe = "is"; - validationContext.PipelineResult.AddError(new ParseError( $"The members {string.Join(", ", groupMembers.Select(m => m.Name))} " + + validationContext.AddError(new ParseError( $"The members {string.Join(", ", groupMembers.Select(m => m.Name))} " + $"must all be used if one is used. {string.Join(", ", missingMembers.Select(m => m.Name))} " + $"{(missingMembers.Skip(1).Any() ? pluralToBe : singularToBe)} missing.")); } diff --git a/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs b/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs index 2c9d6ec4d5..cfab72a183 100644 --- a/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs +++ b/src/System.CommandLine.Subsystems/Validation/RangeValidator.cs @@ -24,7 +24,7 @@ public override void Validate(object? value, CliValueSymbol valueSymbol, } if (valueCondition.MustHaveValidator) { - validationContext.PipelineResult.AddError(new ParseError($"Range validator missing for {valueSymbol.Name}")); + validationContext.AddError(new ParseError($"Range validator missing for {valueSymbol.Name}")); } } diff --git a/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs b/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs index cdd7e184d7..3f22a12280 100644 --- a/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs +++ b/src/System.CommandLine.Subsystems/Validation/ValidationContext.cs @@ -1,20 +1,57 @@ // 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.Parsing; +using System.CommandLine.ValueSources; +using static System.Runtime.InteropServices.JavaScript.JSType; + namespace System.CommandLine.Validation; -// TODO: Remove this class. All of the things it contains are in the PipelineResult, except the ValidationSubsystem currently -// running, if there are multiple. The scenario where that is needed seems unlikely. +/// +/// Provides the context for IValidator implementations +/// public class ValidationContext { - public ValidationContext(PipelineResult pipelineResult, ValidationSubsystem validationSubsystem) + private PipelineResult pipelineResult { get; } + + internal ValidationContext(PipelineResult pipelineResult, ValidationSubsystem validationSubsystem) { - PipelineResult = pipelineResult; + this.pipelineResult = pipelineResult; ValidationSubsystem = validationSubsystem; } - public PipelineResult PipelineResult { get; } - public Pipeline Pipeline => PipelineResult.Pipeline; - public ValidationSubsystem ValidationSubsystem { get; } - public ParseResult? ParseResult => PipelineResult.ParseResult; + /// + /// Adds an error to the PipelineContext. + /// + /// The to add + public void AddError(ParseError error) + => pipelineResult.AddError(error); + + /// + /// Gets the value for an option or argument. + /// + /// The symbol to get the value for. + /// + public object? GetValue(CliValueSymbol valueSymbol) + => pipelineResult.GetValue(valueSymbol); + + /// + /// Gets the for the option or argument, if the user entered a value. + /// + /// The symbol to get the ValueResult for. + /// The ValueResult for the option or argument, or null if the user did not enter a value. + public CliValueResult? GetValueResult(CliValueSymbol valueSymbol) + => pipelineResult.GetValueResult(valueSymbol); + + /// + /// Tries to get the value for a and returns it a an `out` parameter. + /// + /// The type of the value to retrieve + /// The to query for its result. + /// An output parameter that contains the value, if it is found. + /// True if the succeeded, otherwise false. + public bool TryGetTypedValue(ValueSource valueSource, out T? value) + => valueSource.TryGetTypedValue(pipelineResult, out value); + + internal ValidationSubsystem ValidationSubsystem { get; } } diff --git a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs index 076ec829b3..e3dd610b38 100644 --- a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs @@ -74,8 +74,8 @@ private void ValidateValue(CliValueSymbol valueSymbol, ValidationContext validat return; // nothing to do } - var value = validationContext.PipelineResult.GetValue(valueSymbol); - var valueResult = validationContext.ParseResult?.GetValueResult(valueSymbol); + var value = validationContext.GetValue(valueSymbol); + var valueResult = validationContext.GetValueResult(valueSymbol); foreach (var condition in valueConditions) { ValidateValueCondition(value, valueSymbol, valueResult, condition, validationContext); @@ -120,7 +120,7 @@ private void ValidateValueCondition(object? value, CliValueSymbol valueSymbol, C { if (condition.MustHaveValidator) { - validationContext.PipelineResult.AddError(new ParseError($"{valueSymbol.Name} must have {condition.Name} validator.")); + validationContext.AddError(new ParseError($"{valueSymbol.Name} must have {condition.Name} validator.")); } return; } @@ -166,7 +166,7 @@ private void ValidateCommandCondition(CliCommandResult commandResult, CommandCon { if (condition.MustHaveValidator) { - validationContext.PipelineResult.AddError(new ParseError($"{commandResult.Command.Name} must have {condition.Name} validator.")); + validationContext.AddError(new ParseError($"{commandResult.Command.Name} must have {condition.Name} validator.")); } return; } diff --git a/src/System.CommandLine.Subsystems/ValueCondition.cs b/src/System.CommandLine.Subsystems/ValueCondition.cs index 9c20f7f45e..e7f3d8bf77 100644 --- a/src/System.CommandLine.Subsystems/ValueCondition.cs +++ b/src/System.CommandLine.Subsystems/ValueCondition.cs @@ -3,8 +3,22 @@ namespace System.CommandLine; +/// +/// The base class for all conditions. Conditions describe aspects of +/// symbol results, including restrictions used for validation. +/// +/// public abstract class ValueCondition(string name) { + /// + /// Whether a diagnostic should be reported if there is no validator. + /// Conditions may be used for other purposes, such as completions and + /// not require validation. + /// public virtual bool MustHaveValidator { get; } = true; + + /// + /// The name of the ValueCondition. + /// public string Name { get; } = name; } diff --git a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs index 76928d68af..1ca563b7e4 100644 --- a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs @@ -4,8 +4,20 @@ namespace System.CommandLine; +/// +/// Contains the extension methods that are used to create value conditions +/// public static class ValueConditionAnnotationExtensions { + /// + /// Set the upper and/or lower bound values of the range. + /// + /// The type of the bounds. + /// The option or argument the range applies to. + /// The lower bound of the range. + /// The upper bound of the range. + // TODO: Add RangeBounds + // TODO: You should not have to set both...why not nullable? public static void SetRange(this CliValueSymbol symbol, T lowerBound, T upperBound) where T : IComparable { @@ -14,33 +26,35 @@ public static void SetRange(this CliValueSymbol symbol, T lowerBound, T upper symbol.SetValueCondition(range); } - public static void SetRange(this CliValueSymbol symbol, ValueSource lowerBound, T upperBound) - where T : IComparable - { - var range = new Range(lowerBound, upperBound); - - symbol.SetValueCondition(range); - } - - public static void SetRange(this CliValueSymbol symbol, T lowerBound, ValueSource upperBound) - where T : IComparable - { - var range = new Range(lowerBound, upperBound); - - symbol.SetValueCondition(range); - } - + /// + /// Set the upper and/or lower bound via ValueSource. Implicit conversions means this + /// generally just works with any . + /// + /// The type of the bounds. + /// The option or argument the range applies to. + /// The that is the lower bound of the range. + /// The that is the upper bound of the range. + // TODO: Add RangeBounds + // TODO: You should not have to set both...why not nullable? public static void SetRange(this CliValueSymbol symbol, ValueSource lowerBound, ValueSource upperBound) - where T : IComparable + where T : IComparable + // TODO: You should not have to set both...why not nullable? { var range = new Range(lowerBound, upperBound); symbol.SetValueCondition(range); } - public static void SetInclusiveGroup(this CliCommand symbol, IEnumerable group) - => symbol.SetValueCondition(new InclusiveGroup(group)); + /// + /// Indicates that there is an inclusive group of options and arguments for the command. All + /// members of an inclusive must be present, or none can be present. + /// + /// The command the inclusive group applies to. + /// The group of options and arguments that must all be present, or none can be present. + public static void SetInclusiveGroup(this CliCommand command, IEnumerable group) + => command.SetValueCondition(new InclusiveGroup(group)); + // TODO: This should not be public if ValueConditions are not public public static void SetValueCondition(this TValueSymbol symbol, TValueCondition valueCondition) where TValueSymbol : CliValueSymbol where TValueCondition : ValueCondition @@ -53,33 +67,63 @@ public static void SetValueCondition(this TValueS valueConditions.Add(valueCondition); } - public static void SetValueCondition(this CliCommand symbol, TValueCondition valueCondition) - where TValueCondition : CommandCondition + // TODO: This should not be public if ValueConditions are not public + public static void SetValueCondition(this CliCommand symbol, TCommandCondition commandCondition) + where TCommandCondition : CommandCondition { if (!symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions)) { valueConditions = []; symbol.SetAnnotation(ValueConditionAnnotations.ValueConditions, valueConditions); } - valueConditions.Add(valueCondition); + valueConditions.Add(commandCondition); } + /// + /// Gets a list of conditions on an option or argument. + /// + /// The option or argument to get the conditions for. + /// The conditions that have been applied to the option or argument. + /// + // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace public static List? GetValueConditions(this CliValueSymbol symbol) => symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) ? valueConditions : null; - public static List? GetCommandConditions(this CliCommand symbol) - => symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) + /// + /// Gets a list of conditions on a command. + /// + /// The command to get the conditions for. + /// The conditions that have been applied to the command. + /// + // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace + public static List? GetCommandConditions(this CliCommand command) + => command.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) ? valueConditions : null; + /// + /// Gets the condition that matches the type, if it exists on this option or argument. + /// + /// The type of condition to return. + /// The option or argument that may contain the condition. + /// The condition if it exists on the option or argument, otherwise null. + // This method feels useful because it clarifies that last should win and returns one, when only one should be applied + // TODO: Consider removing user facing naming, other than the base type, that is Value or CommandCondition and just use Condition public static TCondition? GetValueCondition(this CliValueSymbol symbol) where TCondition : ValueCondition => !symbol.TryGetAnnotation(ValueConditionAnnotations.ValueConditions, out List? valueConditions) ? null : valueConditions.OfType().LastOrDefault(); + /// + /// Gets the condition that matches the type, if it exists on this command. + /// + /// The type of condition to return. + /// The command that may contain the condition. + /// The condition if it exists on the command, otherwise null. + // This method feels useful because it clarifies that last should win and returns one, when only one should be applied public static TCondition? GetCommandCondition(this CliCommand symbol) where TCondition : CommandCondition => !symbol.TryGetAnnotation(ValueConditionAnnotations.ValueConditions, out List? valueConditions) diff --git a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs index e495fd7db7..2d029e5e8e 100644 --- a/src/System.CommandLine.Subsystems/ValueConditions/Range.cs +++ b/src/System.CommandLine.Subsystems/ValueConditions/Range.cs @@ -8,8 +8,11 @@ namespace System.CommandLine.ValueConditions; /// -/// Declares the range for the option or argument. The non-generic version is used by the . +/// public abstract class Range : ValueCondition { protected Range(Type valueType) @@ -17,13 +20,29 @@ protected Range(Type valueType) { ValueType = valueType; } + + /// + /// The type of the symbol the range applies to. + /// public Type ValueType { get; } } -public class Range(ValueSource? lowerBound, ValueSource? upperBound, RangeBounds rangeBound = 0) - : Range(typeof(T)), IValueValidator +/// +/// Declares the range condition for the option or argument. Instances +/// of this method are created via extension methods on +/// +/// The type of the symbol the range applies to. +public class Range : Range, IValueValidator where T : IComparable { + internal Range(ValueSource? lowerBound, ValueSource? upperBound, RangeBounds rangeBound = 0) : base(typeof(T)) + { + LowerBound = lowerBound; + UpperBound = upperBound; + RangeBound = rangeBound; + } + + /// public void Validate(object? value, CliValueSymbol valueSymbol, CliValueResult? valueResult, @@ -37,23 +56,35 @@ public void Validate(object? value, // TODO: Replace the strings we are comparing with a diagnostic ID when we update ParseError if (LowerBound is not null - && LowerBound.TryGetTypedValue(validationContext.PipelineResult, out var lowerValue) + && validationContext.TryGetTypedValue(LowerBound, out var lowerValue) && comparableValue.CompareTo(lowerValue) < 0) { - validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is below the lower bound of {LowerBound}")); + validationContext.AddError(new ParseError($"The value for '{valueSymbol.Name}' is below the lower bound of {LowerBound}")); } if (UpperBound is not null - && UpperBound.TryGetTypedValue(validationContext.PipelineResult, out var upperValue) + && validationContext.TryGetTypedValue(UpperBound, out var upperValue) && comparableValue.CompareTo(upperValue) > 0) { - validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {UpperBound}")); + validationContext.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {UpperBound}")); } } - public ValueSource? LowerBound { get; init; } = lowerBound; - public ValueSource? UpperBound { get; init; } = upperBound; - public RangeBounds RangeBound { get; } = rangeBound; + /// + /// The lower bound of the range. + /// + public ValueSource? LowerBound { get; init; } + + /// + /// The upper bound of the range. + /// + public ValueSource? UpperBound { get; init; } + + /// + /// Whether values of the range are considered part of the + /// range (inclusive) or not (exclusive) + /// + public RangeBounds RangeBound { get; } } diff --git a/src/System.CommandLine.Subsystems/ValueProvider.cs b/src/System.CommandLine.Subsystems/ValueProvider.cs index 6883afc398..f51a1edb63 100644 --- a/src/System.CommandLine.Subsystems/ValueProvider.cs +++ b/src/System.CommandLine.Subsystems/ValueProvider.cs @@ -1,9 +1,7 @@ // 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.Subsystems.Annotations; using System.CommandLine.ValueSources; -using System.Diagnostics.CodeAnalysis; namespace System.CommandLine; From 78e9a093912a4b8b42faa945ad36c9cb74577434 Mon Sep 17 00:00:00 2001 From: Kathleen Dollard Date: Sat, 31 Aug 2024 09:02:55 -0400 Subject: [PATCH 144/150] Made ctor internal --- src/System.CommandLine.Subsystems/Validation/Validator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.CommandLine.Subsystems/Validation/Validator.cs b/src/System.CommandLine.Subsystems/Validation/Validator.cs index 4fded1927d..e33f732c11 100644 --- a/src/System.CommandLine.Subsystems/Validation/Validator.cs +++ b/src/System.CommandLine.Subsystems/Validation/Validator.cs @@ -11,7 +11,7 @@ namespace System.CommandLine.Validation; // TODO: Discuss visibility and custom validators public abstract class Validator { - public Validator(string name, Type valueConditionType, params Type[] moreValueConditionTypes) + internal Validator(string name, Type valueConditionType, params Type[] moreValueConditionTypes) { Name = name; ValueConditionTypes = [valueConditionType, .. moreValueConditionTypes]; From bc885e722b42cae138b1ea5afd1ecf30755840bb Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 17 Sep 2024 23:11:07 -0400 Subject: [PATCH 145/150] Replace per-subsystem annotation providers with pipeline annotation resolver This simplifies things and means that subsystem authors have one centralized place to get annotation values, instead of having to know which subsytem to use. --- .../AlternateSubsystems.cs | 10 +- .../CompletionSubsystem.cs | 4 +- .../Directives/DiagramSubsystem.cs | 4 +- .../Directives/DirectiveSubsystem.cs | 4 +- .../Directives/ResponseSubsystem.cs | 2 +- .../ErrorReportingSubsystem.cs | 4 +- .../HelpAnnotationExtensions.cs | 21 ++- .../HelpSubsystem.cs | 7 +- .../InvocationSubsystem.cs | 4 +- src/System.CommandLine.Subsystems/Pipeline.cs | 31 ++++- .../Annotations/AnnotationResolver.cs | 126 ++++++++++++++++++ ...tionStorageExtensions.AnnotationStorage.cs | 2 +- .../AnnotationStorageExtensions.cs | 44 +++--- .../Annotations/AnnotationTypeException.cs | 2 +- .../Subsystems/CliSubsystem.cs | 71 +--------- .../Subsystems/IAnnotationProvider.cs | 1 + .../ValidationSubsystem.cs | 4 +- .../ValueConditionAnnotationExtensions.cs | 28 +++- .../VersionSubsystem.cs | 4 +- 19 files changed, 250 insertions(+), 123 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index 5677470b03..864ec95a18 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -30,7 +30,7 @@ public VersionThatUsesHelpData(CliSymbol symbol) public override void Execute(PipelineResult pipelineResult) { - TryGetAnnotation(Symbol, HelpAnnotations.Description, out string? description); + pipelineResult.Pipeline.Annotations.TryGet(Symbol, HelpAnnotations.Description, out string? description); pipelineResult.ConsoleHack.WriteLine(description); pipelineResult.AlreadyHandled = true; pipelineResult.SetSuccess(); @@ -63,12 +63,12 @@ protected internal override void TearDown(PipelineResult pipelineResult) } } - internal class StringDirectiveSubsystem(IAnnotationProvider? annotationProvider = null) - : DirectiveSubsystem("other", SubsystemKind.Diagram, annotationProvider) + internal class StringDirectiveSubsystem() + : DirectiveSubsystem("other", SubsystemKind.Diagram) { } - internal class BooleanDirectiveSubsystem(IAnnotationProvider? annotationProvider = null) - : DirectiveSubsystem("diagram", SubsystemKind.Diagram, annotationProvider) + internal class BooleanDirectiveSubsystem() + : DirectiveSubsystem("diagram", SubsystemKind.Diagram) { } } diff --git a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs index 8fd0156ba0..67039d2f08 100644 --- a/src/System.CommandLine.Subsystems/CompletionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/CompletionSubsystem.cs @@ -17,8 +17,8 @@ namespace System.CommandLine; public class CompletionSubsystem : CliSubsystem { - public CompletionSubsystem(IAnnotationProvider? annotationProvider = null) - : base(CompletionAnnotations.Prefix, SubsystemKind.Completion, annotationProvider) + public CompletionSubsystem() + : base(CompletionAnnotations.Prefix, SubsystemKind.Completion) { } // TODO: Figure out trigger for completions diff --git a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs index 791290a097..421b10851f 100644 --- a/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs @@ -6,8 +6,8 @@ namespace System.CommandLine.Directives; -public class DiagramSubsystem(IAnnotationProvider? annotationProvider = null) - : DirectiveSubsystem("diagram", SubsystemKind.Diagram, annotationProvider) +public class DiagramSubsystem() + : DirectiveSubsystem("diagram", SubsystemKind.Diagram) { //protected internal override bool GetIsActivated(ParseResult? parseResult) // => parseResult is not null && option is not null && parseResult.GetValue(option); diff --git a/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs index efb51bceee..3fb43e18ea 100644 --- a/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/DirectiveSubsystem.cs @@ -13,8 +13,8 @@ public abstract class DirectiveSubsystem : CliSubsystem public string Id { get; } public Location? Location { get; private set; } - public DirectiveSubsystem(string name, SubsystemKind kind, IAnnotationProvider? annotationProvider = null, string? id = null) - : base(name, kind, annotationProvider: annotationProvider) + public DirectiveSubsystem(string name, SubsystemKind kind, string? id = null) + : base(name, kind) { Id = id ?? name; } diff --git a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs index c626348e28..421c89256a 100644 --- a/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Directives/ResponseSubsystem.cs @@ -7,7 +7,7 @@ namespace System.CommandLine.Directives; public class ResponseSubsystem() - : CliSubsystem("Response", SubsystemKind.Response, null) + : CliSubsystem("Response", SubsystemKind.Response) { public bool Enabled { get; set; } diff --git a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs index 7c7432616b..27f43ab9f2 100644 --- a/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ErrorReportingSubsystem.cs @@ -15,8 +15,8 @@ namespace System.CommandLine; /// public class ErrorReportingSubsystem : CliSubsystem { - public ErrorReportingSubsystem(IAnnotationProvider? annotationProvider = null) - : base(ErrorReportingAnnotations.Prefix, SubsystemKind.ErrorReporting, annotationProvider) + public ErrorReportingSubsystem() + : base(ErrorReportingAnnotations.Prefix, SubsystemKind.ErrorReporting) { } protected internal override bool GetIsActivated(ParseResult? parseResult) diff --git a/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs index 8cb06c594d..82b37c3dee 100644 --- a/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/HelpAnnotationExtensions.cs @@ -40,11 +40,28 @@ public static void SetDescription(this TSymbol symbol, string descripti /// The symbol /// The symbol description if any, otherwise /// - /// This is intended to be called by CLI authors. Subsystems should instead call , - /// values from the subsystem's . + /// This is intended to be called by CLI authors. Subsystem authors should instead call + /// to get values from + /// the pipeline's , which takes dynamic providers into account. /// public static string? GetDescription(this TSymbol symbol) where TSymbol : CliSymbol { return symbol.GetAnnotationOrDefault(HelpAnnotations.Description); } + + /// + /// Get the help description for the from the , + /// which takes dynamic providers into account. + /// + /// The type of the symbol + /// The symbol + /// The symbol description if any, otherwise + /// + /// This is intended to be called by subsystem authors. CLI authors should instead call + /// to get the value associated directly with the symbol. + /// + public static string? GetDescription(this AnnotationResolver resolver, TSymbol symbol) where TSymbol : CliSymbol + { + return resolver.GetAnnotationOrDefault(symbol, HelpAnnotations.Description); + } } diff --git a/src/System.CommandLine.Subsystems/HelpSubsystem.cs b/src/System.CommandLine.Subsystems/HelpSubsystem.cs index ca4d99c2ae..29dd06aff4 100644 --- a/src/System.CommandLine.Subsystems/HelpSubsystem.cs +++ b/src/System.CommandLine.Subsystems/HelpSubsystem.cs @@ -15,8 +15,8 @@ namespace System.CommandLine; // var command = new CliCommand("greet") // .With(help.Description, "Greet the user"); // -public class HelpSubsystem(IAnnotationProvider? annotationProvider = null) - : CliSubsystem(HelpAnnotations.Prefix, SubsystemKind.Help, annotationProvider) +public class HelpSubsystem() + : CliSubsystem(HelpAnnotations.Prefix, SubsystemKind.Help) { /// /// Gets the help option, which allows the user to customize @@ -44,7 +44,4 @@ public override void Execute(PipelineResult pipelineResult) pipelineResult.ConsoleHack.WriteLine("Help me!"); pipelineResult.SetSuccess(); } - - public bool TryGetDescription(CliSymbol symbol, out string? description) - => TryGetAnnotation(symbol, HelpAnnotations.Description, out description); } diff --git a/src/System.CommandLine.Subsystems/InvocationSubsystem.cs b/src/System.CommandLine.Subsystems/InvocationSubsystem.cs index a451065831..4dec80e8b1 100644 --- a/src/System.CommandLine.Subsystems/InvocationSubsystem.cs +++ b/src/System.CommandLine.Subsystems/InvocationSubsystem.cs @@ -6,6 +6,6 @@ namespace System.CommandLine; -public class InvocationSubsystem(IAnnotationProvider? annotationProvider = null) - : CliSubsystem(InvocationAnnotations.Prefix, SubsystemKind.Invocation, annotationProvider) +public class InvocationSubsystem() + : CliSubsystem(InvocationAnnotations.Prefix, SubsystemKind.Invocation) {} diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 837fdea398..5704bc45bf 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -4,6 +4,7 @@ using System.CommandLine.Directives; using System.CommandLine.Parsing; using System.CommandLine.Subsystems; +using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine; @@ -17,6 +18,7 @@ public partial class Pipeline private readonly PipelinePhase invocationPhase = new(SubsystemKind.Invocation); private readonly PipelinePhase errorReportingPhase = new(SubsystemKind.ErrorReporting); private readonly IEnumerable phases; + private readonly List annotationProviders; /// /// Creates an instance of the pipeline using standard features. @@ -34,14 +36,15 @@ public static Pipeline Create(HelpSubsystem? help = null, VersionSubsystem? version = null, CompletionSubsystem? completion = null, DiagramSubsystem? diagram = null, - ErrorReportingSubsystem? errorReporting = null) - => new() + ErrorReportingSubsystem? errorReporting = null, + IEnumerable? annotationProviders = null) + => new(annotationProviders) { Help = help ?? new HelpSubsystem(), Version = version ?? new VersionSubsystem(), Completion = completion ?? new CompletionSubsystem(), Diagram = diagram ?? new DiagramSubsystem(), - ErrorReporting = errorReporting ?? new ErrorReportingSubsystem(), + ErrorReporting = errorReporting ?? new ErrorReportingSubsystem() }; /// @@ -54,7 +57,7 @@ public static Pipeline Create(HelpSubsystem? help = null, public static Pipeline CreateEmpty() => new(); - private Pipeline() + private Pipeline(IEnumerable? annotationProviders = null) { Response = new ResponseSubsystem(); Invocation = new InvocationSubsystem(); @@ -68,6 +71,11 @@ private Pipeline() diagramPhase, completionPhase, helpPhase, versionPhase, validationPhase, invocationPhase, errorReportingPhase ]; + + this.annotationProviders = annotationProviders is not null + ? [..annotationProviders] + : []; + Annotations = new(this.annotationProviders); } /// @@ -186,6 +194,21 @@ public ErrorReportingSubsystem? ErrorReporting /// public ResponseSubsystem Response { get; } + /// + /// Gets the annotation resolver + /// + public AnnotationResolver Annotations { get; } + + /// + /// Gets the list of annotation providers registered with the pipeline + /// + public IReadOnlyList AnnotationProviders => annotationProviders; + + /// + /// Adds an annotation provider to the pipeline + /// + public void AddAnnotationProvider(IAnnotationProvider annotationProvider) => annotationProviders.Add(annotationProvider); + public ParseResult Parse(CliConfiguration configuration, string rawInput) => Parse(configuration, CliParser.SplitCommandLine(rawInput).ToArray()); diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs new file mode 100644 index 0000000000..6831a75876 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs @@ -0,0 +1,126 @@ +// 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.CodeAnalysis; + +namespace System.CommandLine.Subsystems.Annotations; + +/// +/// Provides a resolved annotation value for a given , +/// taking into account both the values stored directly on the symbol via extension methods +/// and any values from providers. +/// +/// +/// The will be enumerated each time an annotation value is requested. +/// It may be modified after the resolver is created. +/// +public class AnnotationResolver(ICollection providers) +{ + private readonly IEnumerable providers = providers ?? throw new ArgumentNullException(nameof(providers)); + + /// + /// Attempt to retrieve the 's value for the annotation . This will check any + /// annotation providers that were passed to the constructor, and the internal per-symbol annotation storage. + /// + /// + /// The expected type of the annotation value. If the type does not match, a will be thrown. + /// If the annotation allows multiple types for its values, and a type parameter cannot be determined statically, + /// use to access the annotation value without checking its type. + /// + /// The symbol the value is attached to + /// + /// The identifier for the annotation value to be retrieved. + /// For example, the annotation identifier for the help description is . + /// + /// An out parameter to contain the result + /// True if successful + /// + /// This is intended for use by developers defining custom annotation IDs. Anyone defining an annotation + /// ID should also define an accessor extension method on extension method + /// on that subsystem authors can use to access the annotation value, such as + /// . + /// + /// If the annotation value does not have a single expected type for this symbol, use the + /// overload instead. + /// + /// + public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) + { + foreach (var provider in providers) + { + if (provider.TryGet(symbol, annotationId, out object? rawValue)) + { + if (rawValue is TValue expectedTypeValue) + { + value = expectedTypeValue; + return true; + } + throw new AnnotationTypeException(annotationId, typeof(TValue), rawValue?.GetType(), provider); + } + } + + return symbol.TryGetAnnotation(annotationId, out value); + } + + /// + /// Attempt to retrieve the 's value for the annotation . This will check any + /// annotation providers that were passed to the constructor, and the internal per-symbol annotation storage. + /// + /// The symbol the value is attached to + /// + /// The identifier for the annotation value to be retrieved. + /// For example, the annotation identifier for the help description is . + /// + /// An out parameter to contain the result + /// True if successful + /// + /// This is intended for use by developers defining custom annotation IDs. Anyone defining an annotation + /// ID should also define an accessor extension method on extension method + /// on that subsystem authors can use to access the annotation value, such as + /// . + /// + /// If the expected type of the annotation value is known, use the + /// overload instead. + /// + /// + public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out object? value) + { + foreach (var provider in providers) + { + if (provider.TryGet(symbol, annotationId, out value)) + { + return true; + } + } + + return symbol.TryGetAnnotation(annotationId, out value); + } + + /// + /// Attempt to retrieve the 's value for the annotation . This will check any + /// annotation providers that were passed to the constructor, and the internal per-symbol annotation storage. If the + /// annotation value is not found, the default value for will be returned. + /// + /// The type of the annotation value + /// The symbol that is annotated + /// + /// The identifier for the annotation. For example, the annotation identifier for the help description + /// is . + /// + /// The annotation value, if successful, otherwise default + /// + /// This is intended for use by developers defining custom annotation IDs. Anyone defining an annotation + /// ID should also define an accessor extension method on extension method + /// on that subsystem authors can use to access the annotation value, such as + /// . + /// + public TValue? GetAnnotationOrDefault(CliSymbol symbol, AnnotationId annotationId) + { + if (TryGet(symbol, annotationId, out TValue? value)) + { + return value; + } + + return default; + } +} \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs index 58341d3bf1..c9cc4a7031 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.AnnotationStorage.cs @@ -7,7 +7,7 @@ namespace System.CommandLine.Subsystems.Annotations; partial class AnnotationStorageExtensions { - class AnnotationStorage : IAnnotationProvider + class AnnotationStorage { record struct AnnotationKey(CliSymbol symbol, string prefix, string id) { diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs index 5a8a4be569..6e2a65cf56 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs @@ -107,9 +107,10 @@ public static TSymbol WithAnnotation(this TSymbol symbol, AnnotationId /// This is intended to be called by the implementation of specialized ID-specific accessors for CLI authors such as . /// /// - /// Subsystems should not call it directly, as it does not account for values from the subsystem's . They should instead call - /// or an ID-specific accessor on the subsystem such - /// . + /// Subsystems should not call this directly, as it does not account for values from the pipeline's . + /// They should instead access annotations from the see cref="Pipeline.Annotations"/> property using + /// or an ID-specific + /// extension method such as . /// /// public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) @@ -129,24 +130,29 @@ public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId } /// - /// Attempts to get the value for the annotation associated with the in the internal annotation - /// storage used to store values set via . + /// Attempts to get the value for the annotation associated with the + /// in the internal annotation storage used to store values set via + /// . /// /// The symbol that is annotated /// - /// The identifier for the annotation. For example, the annotation identifier for the help description is . + /// The identifier for the annotation. For example, the annotation identifier for the help + /// description is . /// /// The annotation value, if successful, otherwise default /// True if successful /// - /// If the expected type of the annotation value is known, use the overload instead. + /// If the expected type of the annotation value is known, use the + /// overload instead. /// - /// This is intended to be called by the implementation of specialized ID-specific accessors for CLI authors such as . + /// This is intended to be called by the implementation of specialized ID-specific accessors for + /// CLI authors such as . /// /// - /// Subsystems should not call it directly, as it does not account for values from the subsystem's . They should instead call - /// or an ID-specific accessor on the subsystem such - /// . + /// Subsystems should not call this directly, as it does not account for values from the pipeline's . + /// They should instead access annotations from the see cref="Pipeline.Annotations"/> property using + /// or an ID-specific + /// extension method such as . /// /// public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out object? value) @@ -161,20 +167,22 @@ public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotati } /// - /// Attempts to get the value for the annotation associated with the in the internal annotation - /// storage used to store values set via . + /// Attempts to get the value for the annotation associated with the + /// in the internal annotation storage used to store values set via + /// . /// /// The type of the annotation value /// The symbol that is annotated /// - /// The identifier for the annotation. For example, the annotation identifier for the help description is . + /// The identifier for the annotation. For example, the annotation identifier for the help description + /// is . /// /// The annotation value, if successful, otherwise default /// - /// This is intended to be called by specialized ID-specific accessors for CLI authors such as . - /// Subsystems should not call it, as it does not account for values from the subsystem's . They should instead call - /// or an ID-specific accessor on the subsystem such - /// . + /// Subsystems should not call this directly, as it does not account for values from the pipeline's . + /// They should instead access annotations from the see cref="Pipeline.Annotations"/> property using + /// or an ID-specific + /// extension method such as . /// public static TValue? GetAnnotationOrDefault(this CliSymbol symbol, AnnotationId annotationId) { diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs index 4d1a19aded..313a274ad1 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs @@ -30,4 +30,4 @@ public override string Message $"This may be an authoring error in a typed annotation accessor, or the annotation may have be stored directly with the incorrect type, bypassing the typed accessors."; } } -} \ No newline at end of file +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs index 49e6f98af7..640a0cebf2 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs @@ -12,10 +12,9 @@ namespace System.CommandLine.Subsystems; /// public abstract class CliSubsystem { - protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProvider? annotationProvider) + protected CliSubsystem(string name, SubsystemKind subsystemKind) { Name = name; - _annotationProvider = annotationProvider; Kind = subsystemKind; } @@ -30,74 +29,6 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv public SubsystemKind Kind { get; } public AddToPhaseBehavior RecommendedAddToPhaseBehavior { get; } - private readonly IAnnotationProvider? _annotationProvider; - - /// - /// Attempt to retrieve the 's value for the annotation . This will check the - /// annotation provider that was passed to the subsystem constructor, and the internal annotation storage. - /// - /// - /// The expected type of the annotation value. If the type does not match, a will be thrown. - /// If the annotation allows multiple types for its values, and a type parameter cannot be determined statically, - /// use to access the annotation value without checking its type. - /// - /// The symbol the value is attached to - /// - /// The identifier for the annotation value to be retrieved. - /// For example, the annotation identifier for the help description is . - /// - /// An out parameter to contain the result - /// True if successful - /// - /// If the annotation value does not have a single expected type for this symbol, use the overload instead. - /// - /// Subsystem authors must use this to access annotation values, as it respects the subsystem's if it has one. - /// This value is protected because it is intended for use only by subsystem authors. It calls - /// - /// - protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) - { - if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, annotationId, out object? rawValue)) - { - if (rawValue is TValue expectedTypeValue) - { - value = expectedTypeValue; - return true; - } - throw new AnnotationTypeException(annotationId, typeof(TValue), rawValue?.GetType(), _annotationProvider); - } - - return symbol.TryGetAnnotation(annotationId, out value); - } - - /// - /// Attempt to retrieve the 's value for the annotation . This will check the - /// annotation provider that was passed to the subsystem constructor, and the internal annotation storage. - /// - /// The symbol the value is attached to - /// - /// The identifier for the annotation value to be retrieved. - /// For example, the annotation identifier for the help description is . - /// - /// An out parameter to contain the result - /// True if successful - /// - /// If the expected type of the annotation value is known, use the overload instead. - /// - /// Subsystem authors must use this to access annotation values, as it respects the subsystem's if it has one. - /// This value is protected because it is intended for use only by subsystem authors. It calls - /// - /// - protected internal bool TryGetAnnotation(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out object? value) - { - if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, annotationId, out value)) - { - return true; - } - - return symbol.TryGetAnnotation(annotationId, out value); - } - /// /// The subystem executes, even if another subsystem has handled the operation. This is expected to be used in things like error reporting. /// diff --git a/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs b/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs index dd4d9e4fd5..a1bd2ede9d 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs @@ -1,6 +1,7 @@ // 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.Parsing; using System.CommandLine.Subsystems.Annotations; using System.Diagnostics.CodeAnalysis; diff --git a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs index e3dd610b38..58602e5f45 100644 --- a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs @@ -14,8 +14,8 @@ public sealed class ValidationSubsystem : CliSubsystem private Dictionary valueValidators = []; private Dictionary commandValidators = []; - private ValidationSubsystem(IAnnotationProvider? annotationProvider = null) - : base("", SubsystemKind.Validation, annotationProvider) + private ValidationSubsystem() + : base("", SubsystemKind.Validation) { } public static ValidationSubsystem Create() diff --git a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs index 1ca563b7e4..1729541d38 100644 --- a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs @@ -84,25 +84,49 @@ public static void SetValueCondition(this CliCommand symbol, /// /// The option or argument to get the conditions for. /// The conditions that have been applied to the option or argument. - /// + /// // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace public static List? GetValueConditions(this CliValueSymbol symbol) => symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) ? valueConditions : null; + /// + /// Gets a list of conditions on an option or argument. + /// + /// The option or argument to get the conditions for. + /// The conditions that have been applied to the option or argument. + /// + // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace + public static List? GetValueConditions(this AnnotationResolver resolver, CliValueSymbol symbol) + => resolver.TryGet>(symbol, ValueConditionAnnotations.ValueConditions, out var valueConditions) + ? valueConditions + : null; + /// /// Gets a list of conditions on a command. /// /// The command to get the conditions for. /// The conditions that have been applied to the command. - /// + /// // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace public static List? GetCommandConditions(this CliCommand command) => command.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) ? valueConditions : null; + /// + /// Gets a list of conditions on a command. + /// + /// The command to get the conditions for. + /// The conditions that have been applied to the command. + /// + // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace + public static List? GetCommandConditions(this AnnotationResolver resolver, CliCommand command) + => resolver.TryGet>(command, ValueConditionAnnotations.ValueConditions, out var valueConditions) + ? valueConditions + : null; + /// /// Gets the condition that matches the type, if it exists on this option or argument. /// diff --git a/src/System.CommandLine.Subsystems/VersionSubsystem.cs b/src/System.CommandLine.Subsystems/VersionSubsystem.cs index 1aac8b75a1..c8b8c784dc 100644 --- a/src/System.CommandLine.Subsystems/VersionSubsystem.cs +++ b/src/System.CommandLine.Subsystems/VersionSubsystem.cs @@ -11,8 +11,8 @@ public class VersionSubsystem : CliSubsystem { private string? specificVersion = null; - public VersionSubsystem(IAnnotationProvider? annotationProvider = null) - : base(VersionAnnotations.Prefix, SubsystemKind.Version, annotationProvider) + public VersionSubsystem() + : base(VersionAnnotations.Prefix, SubsystemKind.Version) { } From 0914d565bfd6c1eaf0808e6c7586720a3e6a3bdf Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 18 Sep 2024 22:49:03 -0400 Subject: [PATCH 146/150] Pass additional context to annotation providers This allows more powerful dynamic resolution of annotation values --- .../AlternateSubsystems.cs | 2 +- src/System.CommandLine.Subsystems/Pipeline.cs | 6 ---- .../PipelineResult.cs | 4 +++ .../Annotations/AnnotationResolveContext.cs | 36 +++++++++++++++++++ .../Annotations/AnnotationResolver.cs | 13 ++++--- .../Annotations/IAnnotationProvider.cs | 22 ++++++++++++ .../Subsystems/IAnnotationProvider.cs | 16 --------- 7 files changed, 72 insertions(+), 27 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolveContext.cs create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/IAnnotationProvider.cs delete mode 100644 src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs diff --git a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs index 864ec95a18..c29ee0026f 100644 --- a/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs +++ b/src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs @@ -30,7 +30,7 @@ public VersionThatUsesHelpData(CliSymbol symbol) public override void Execute(PipelineResult pipelineResult) { - pipelineResult.Pipeline.Annotations.TryGet(Symbol, HelpAnnotations.Description, out string? description); + pipelineResult.Annotations.TryGet(Symbol, HelpAnnotations.Description, out string? description); pipelineResult.ConsoleHack.WriteLine(description); pipelineResult.AlreadyHandled = true; pipelineResult.SetSuccess(); diff --git a/src/System.CommandLine.Subsystems/Pipeline.cs b/src/System.CommandLine.Subsystems/Pipeline.cs index 5704bc45bf..0f8711efd6 100644 --- a/src/System.CommandLine.Subsystems/Pipeline.cs +++ b/src/System.CommandLine.Subsystems/Pipeline.cs @@ -75,7 +75,6 @@ private Pipeline(IEnumerable? annotationProviders = null) this.annotationProviders = annotationProviders is not null ? [..annotationProviders] : []; - Annotations = new(this.annotationProviders); } /// @@ -194,11 +193,6 @@ public ErrorReportingSubsystem? ErrorReporting /// public ResponseSubsystem Response { get; } - /// - /// Gets the annotation resolver - /// - public AnnotationResolver Annotations { get; } - /// /// Gets the list of annotation providers registered with the pipeline /// diff --git a/src/System.CommandLine.Subsystems/PipelineResult.cs b/src/System.CommandLine.Subsystems/PipelineResult.cs index 55b5f4963e..cb1763c3d1 100644 --- a/src/System.CommandLine.Subsystems/PipelineResult.cs +++ b/src/System.CommandLine.Subsystems/PipelineResult.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Parsing; +using System.CommandLine.Subsystems.Annotations; namespace System.CommandLine; @@ -18,6 +19,7 @@ public PipelineResult(ParseResult parseResult, string rawInput, Pipeline? pipeli Pipeline = pipeline ?? Pipeline.CreateEmpty(); ConsoleHack = consoleHack ?? new ConsoleHack(); valueProvider = new ValueProvider(this); + Annotations = new AnnotationResolver(this); } public ParseResult ParseResult { get; } @@ -27,6 +29,8 @@ public PipelineResult(ParseResult parseResult, string rawInput, Pipeline? pipeli public Pipeline Pipeline { get; } public ConsoleHack ConsoleHack { get; } + public AnnotationResolver Annotations { get; } + public bool AlreadyHandled { get; set; } public int ExitCode { get; set; } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolveContext.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolveContext.cs new file mode 100644 index 0000000000..a13fffea1c --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolveContext.cs @@ -0,0 +1,36 @@ +// 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.Parsing; + +namespace System.CommandLine.Subsystems.Annotations; + +/// +/// Additional context that is passed to . +/// +/// +/// This class exists so that additional context properties can be added without +/// breaking existing implementations. +/// +/// This is intended to be usable independently of the pipeline. For example, a method could be +/// implemented that takes a and prints help output based on the help +/// annotations in the tree, which would then be usable by developers who +/// are using the API directly. +/// +/// +public class AnnotationResolveContext(ParseResult parseResult) +{ + public AnnotationResolveContext(PipelineResult pipelineResult) + : this(pipelineResult.ParseResult) + { + } + + /// + /// The for which annotations are being resolved. + /// + /// + /// This may be used to resolve different values for an annotation based on the parents of the symbol, + /// or based on values of other symbols in the parse result. + /// + public ParseResult ParseResult { get; } = parseResult; +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs index 6831a75876..05c07cd5f5 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs @@ -10,13 +10,18 @@ namespace System.CommandLine.Subsystems.Annotations; /// taking into account both the values stored directly on the symbol via extension methods /// and any values from providers. /// +/// The providers from which annotation values will be resolved +/// The context for resolving annotation values /// /// The will be enumerated each time an annotation value is requested. /// It may be modified after the resolver is created. /// -public class AnnotationResolver(ICollection providers) +public class AnnotationResolver(IEnumerable providers, AnnotationResolveContext context) { - private readonly IEnumerable providers = providers ?? throw new ArgumentNullException(nameof(providers)); + public AnnotationResolver(PipelineResult pipelineResult) + : this(pipelineResult.Pipeline.AnnotationProviders, new AnnotationResolveContext(pipelineResult)) + { + } /// /// Attempt to retrieve the 's value for the annotation . This will check any @@ -48,7 +53,7 @@ public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNull { foreach (var provider in providers) { - if (provider.TryGet(symbol, annotationId, out object? rawValue)) + if (provider.TryGet(symbol, annotationId, context, out object? rawValue)) { if (rawValue is TValue expectedTypeValue) { @@ -87,7 +92,7 @@ public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(tru { foreach (var provider in providers) { - if (provider.TryGet(symbol, annotationId, out value)) + if (provider.TryGet(symbol, annotationId, context, out value)) { return true; } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/IAnnotationProvider.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/IAnnotationProvider.cs new file mode 100644 index 0000000000..131769e9c0 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/IAnnotationProvider.cs @@ -0,0 +1,22 @@ +// 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.CodeAnalysis; + +namespace System.CommandLine.Subsystems.Annotations; + +/// +/// Alternative storage of annotations, enabling lazy loading and dynamic annotations. +/// +public interface IAnnotationProvider +{ + /// + /// Try to get the value of the annotation with the given for the . + /// + /// Additional context that may be used when resolving the annotation value. + /// The symbol + /// The annotation identifier + /// The annotation value + /// if the symbol was resolved, otherwise + bool TryGet(CliSymbol symbol, AnnotationId annotationId, AnnotationResolveContext context, [NotNullWhen(true)] out object? value); +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs b/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.cs deleted file mode 100644 index a1bd2ede9d..0000000000 --- a/src/System.CommandLine.Subsystems/Subsystems/IAnnotationProvider.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.Parsing; -using System.CommandLine.Subsystems.Annotations; -using System.Diagnostics.CodeAnalysis; - -namespace System.CommandLine.Subsystems; - -/// -/// Alternative storage of annotations, enabling lazy loading and dynamic annotations. -/// -public interface IAnnotationProvider -{ - bool TryGet(CliSymbol symbol, AnnotationId id, [NotNullWhen(true)] out object? value); -} From 71f3ca47a78bce31b39804e05d024f52cc4242f9 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 19 Sep 2024 01:17:15 -0400 Subject: [PATCH 147/150] Implement multivalued annotations Annotations may be defined as having multiple values, in which case multiple values may be added to the internal storage on the symbol, and providers may return enumerations of values. New annotation enumeration APIs allow enumerating the values from the symbol and the providers. --- .../AnnotationCollectionTypeException.cs | 34 ++++ .../Annotations/AnnotationResolver.cs | 75 +++++++- .../AnnotationStorageExtensions.cs | 171 ++++++++++++++---- .../Annotations/AnnotationTypeException.cs | 12 +- .../ValidationSubsystem.cs | 23 ++- .../ValueConditionAnnotationExtensions.cs | 55 ++---- 6 files changed, 269 insertions(+), 101 deletions(-) create mode 100644 src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationCollectionTypeException.cs diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationCollectionTypeException.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationCollectionTypeException.cs new file mode 100644 index 0000000000..97ba7ea921 --- /dev/null +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationCollectionTypeException.cs @@ -0,0 +1,34 @@ +// 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.Subsystems.Annotations; + +/// +/// Thrown when an annotation collection value does not match the expected type for that . +/// +public class AnnotationCollectionTypeException(AnnotationId annotationId, Type expectedType, Type? actualType, IAnnotationProvider? provider = null) + : AnnotationTypeException(annotationId, expectedType, actualType, provider) +{ + public override string Message + { + get + { + if (Provider is not null) + { + return + $"Typed accessor for annotation '${AnnotationId}' expected collection of values of type " + + $"'{ExpectedType}' but the annotation provider returned an annotation value of type " + + $"'{ActualType?.ToString() ?? "[null]"}'. " + + $"This may be an authoring error in in the annotation provider '{Provider.GetType()}' or in a " + + "typed annotation accessor."; + + } + + return + $"Typed accessor for annotation '${AnnotationId}' expected collection of values of type '{ExpectedType}' " + + $"but the stored annotation value is of type '{ActualType?.ToString() ?? "[null]"}'. " + + $"This may be an authoring error in a typed annotation accessor, or the annotation may have been stored " + + $"directly with the incorrect type, bypassing the typed accessors."; + } + } +} diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs index 05c07cd5f5..f49ce86cad 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs @@ -25,14 +25,14 @@ public AnnotationResolver(PipelineResult pipelineResult) /// /// Attempt to retrieve the 's value for the annotation . This will check any - /// annotation providers that were passed to the constructor, and the internal per-symbol annotation storage. + /// annotation providers that were passed to the resolver, and the internal per-symbol annotation storage. /// /// /// The expected type of the annotation value. If the type does not match, a will be thrown. /// If the annotation allows multiple types for its values, and a type parameter cannot be determined statically, - /// use to access the annotation value without checking its type. + /// use to access the annotation value without checking its type. /// - /// The symbol the value is attached to + /// The symbol that is annotated /// /// The identifier for the annotation value to be retrieved. /// For example, the annotation identifier for the help description is . @@ -69,9 +69,9 @@ public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNull /// /// Attempt to retrieve the 's value for the annotation . This will check any - /// annotation providers that were passed to the constructor, and the internal per-symbol annotation storage. + /// annotation providers that were passed to the resolver, and the internal per-symbol annotation storage. /// - /// The symbol the value is attached to + /// The symbol that is annotated /// /// The identifier for the annotation value to be retrieved. /// For example, the annotation identifier for the help description is . @@ -90,6 +90,13 @@ public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNull /// public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out object? value) { + // a value set directly on the symbol takes precedence over values returned by providers. + if (symbol.TryGetAnnotation(annotationId, out value)) + { + return true; + } + + // Providers are given precedence in the order they were provided to the resolver. foreach (var provider in providers) { if (provider.TryGet(symbol, annotationId, context, out value)) @@ -98,7 +105,7 @@ public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(tru } } - return symbol.TryGetAnnotation(annotationId, out value); + return false; } /// @@ -128,4 +135,60 @@ public bool TryGet(CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(tru return default; } + + /// + /// For an annotation that permits multiple values, enumerate the values associated with + /// the . If the annotation is not set, an empty enumerable will be returned. This will + /// check any annotation providers that were passed to the resolver, and the internal per-symbol annotation storage. + /// + /// + /// The expected type of the annotation value. If the type does not match, a + /// will be thrown. + /// + /// The symbol that is annotated + /// + /// The identifier for the annotation value to be retrieved. + /// For example, the annotation identifier for the help description is . + /// + /// An out parameter to contain the result + /// True if successful + /// + /// This is intended for use by developers defining custom annotation IDs. Anyone defining an annotation + /// ID should also define an accessor extension method on extension method + /// on that subsystem authors can use to access the annotation value, such as + /// . + /// + public IEnumerable Enumerate(CliSymbol symbol, AnnotationId annotationId) + { + // Values added directly on the symbol take precedence over values returned by providers, + // so they are returned first. + // NOTE: EnumerateAnnotations returns these in the reverse order they were added, which means that + // callers that take the first value of a given subtype will get the most recently added value of + // that subtype that the CLI author added to the symbol. + foreach (var value in symbol.EnumerateAnnotations(annotationId)) + { + yield return value; + } + + // Providers are given precedence in the order they were provided to the resolver. + foreach (var provider in providers) + { + if (!provider.TryGet(symbol, annotationId, context, out object? rawValue)) + { + continue; + } + + if (rawValue is IEnumerable expectedTypeValue) + { + foreach (var value in expectedTypeValue) + { + yield return value; + } + } + else + { + throw new AnnotationTypeException(annotationId, typeof(IEnumerable), rawValue?.GetType(), provider); + } + } + } } \ No newline at end of file diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs index 6e2a65cf56..b62f205257 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs @@ -92,8 +92,6 @@ public static TSymbol WithAnnotation(this TSymbol symbol, AnnotationId /// /// /// The expected type of the annotation value. If the type does not match, a will be thrown. - /// If the annotation allows multiple types for its values, and a type parameter cannot be determined statically, - /// use to access the annotation value without checking its type. /// /// The symbol that is annotated /// @@ -102,10 +100,7 @@ public static TSymbol WithAnnotation(this TSymbol symbol, AnnotationId /// The annotation value, if successful, otherwise default /// True if successful /// - /// If the annotation value does not have a single expected type for this symbol, use the overload instead. - /// /// This is intended to be called by the implementation of specialized ID-specific accessors for CLI authors such as . - /// /// /// Subsystems should not call this directly, as it does not account for values from the pipeline's . /// They should instead access annotations from the see cref="Pipeline.Annotations"/> property using @@ -113,9 +108,12 @@ public static TSymbol WithAnnotation(this TSymbol symbol, AnnotationId /// extension method such as . /// /// + /// + /// Thrown when the type of the annotation value does not match the expected type. + /// public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out TValue? value) { - if (TryGetAnnotation(symbol, annotationId, out object? rawValue)) + if (symbolToAnnotationStorage.TryGetValue(symbol, out var storage) && storage.TryGet(symbol, annotationId, out var rawValue)) { if (rawValue is TValue expectedTypeValue) { @@ -132,65 +130,158 @@ public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId /// /// Attempts to get the value for the annotation associated with the /// in the internal annotation storage used to store values set via - /// . + /// . /// + /// The type of the annotation value /// The symbol that is annotated /// - /// The identifier for the annotation. For example, the annotation identifier for the help - /// description is . + /// The identifier for the annotation. For example, the annotation identifier for the help description + /// is . /// - /// The annotation value, if successful, otherwise default - /// True if successful + /// The annotation value, if successful, otherwise default /// - /// If the expected type of the annotation value is known, use the - /// overload instead. - /// - /// This is intended to be called by the implementation of specialized ID-specific accessors for - /// CLI authors such as . - /// - /// - /// Subsystems should not call this directly, as it does not account for values from the pipeline's . - /// They should instead access annotations from the see cref="Pipeline.Annotations"/> property using + /// Subsystems should not call this directly, as it does not account for values from the pipeline's + /// . + /// They should instead access annotations from the property using /// or an ID-specific /// extension method such as . - /// /// - public static bool TryGetAnnotation(this CliSymbol symbol, AnnotationId annotationId, [NotNullWhen(true)] out object? value) + /// + /// Thrown when the type of the annotation value does not match the expected type. + /// + public static TValue? GetAnnotationOrDefault(this CliSymbol symbol, AnnotationId annotationId) { - if (symbolToAnnotationStorage.TryGetValue(symbol, out var storage) && storage.TryGet(symbol, annotationId, out value)) + if (symbol.TryGetAnnotation(annotationId, out TValue? value)) { - return true; + return value; } - value = default; - return false; + return default; } /// - /// Attempts to get the value for the annotation associated with the - /// in the internal annotation storage used to store values set via - /// . + /// For an annotation that permits multiple values, add this value to the collection + /// associated with in the internal annotation storage. /// - /// The type of the annotation value /// The symbol that is annotated - /// - /// The identifier for the annotation. For example, the annotation identifier for the help description - /// is . + /// + /// The identifier for the annotation. For example, the annotation identifier for the help description is . /// - /// The annotation value, if successful, otherwise default + /// The annotation value + public static void AddAnnotation(this CliSymbol symbol, AnnotationId annotationId, object value) + { + var storage = symbolToAnnotationStorage.GetValue(symbol, static (CliSymbol _) => new AnnotationStorage()); + if (!storage.TryGet(symbol, annotationId, out var existingValue)) + { + // avoid creation of the list until we have a second value + storage.Set(symbol, annotationId, value); + return; + } + + if (existingValue is AnnotationList existingList) + { + existingList.Add(value); + return; + } + + storage.Set(symbol, annotationId, new AnnotationList { existingValue, value }); + } + + /// + /// For an annotation that permits multiple values, attempt to remove this value from the collection + /// associated with in the internal annotation storage. + /// + /// The symbol that is annotated + /// + /// The identifier for the annotation. For example, the annotation identifier for the help description is + /// . + /// + /// The annotation value + /// True if the value was removed, false if the value was not found + public static bool RemoveAnnotation(this CliSymbol symbol, AnnotationId annotationId, object value) + { + var storage = symbolToAnnotationStorage.GetValue(symbol, static (CliSymbol _) => new AnnotationStorage()); + if (!storage.TryGet(symbol, annotationId, out var existingValue)) + { + return false; + } + + if (existingValue is not AnnotationList existingList) + { + if (Equals(existingValue, value)) + { + storage.Set(symbol, annotationId, null); + return true; + } + return false; + } + + return existingList.Remove(value); + } + + /// + /// For an annotation that permits multiple values, enumerate the values associated with + /// the . If the annotation is not set, an empty enumerable will be returned. + /// + /// The expected types of the annotation value. + /// If a value type does not match, a will be thrown. + /// The symbol that is annotated + /// + /// The identifier for the annotation. For example, the annotation identifier for the help description is + /// . + /// + /// The annotation values /// - /// Subsystems should not call this directly, as it does not account for values from the pipeline's . - /// They should instead access annotations from the see cref="Pipeline.Annotations"/> property using + /// The values are returned in the reverse order they were added, so that the first value enumerated is the + /// last value added. This means that if callers take the first value of a given subtype, this will give the + /// most recent value of the expected type. + /// + /// This is intended to be called by the implementation of specialized ID-specific accessors for + /// CLI authors such as . + /// + /// + /// Subsystems should not call this directly, as it does not account for values from the pipeline's + /// . + /// They should instead access annotations from the property using /// or an ID-specific /// extension method such as . + /// /// - public static TValue? GetAnnotationOrDefault(this CliSymbol symbol, AnnotationId annotationId) + /// + /// Thrown when the type of the annotation value does not match the expected type. + /// + public static IEnumerable EnumerateAnnotations(this CliSymbol symbol, AnnotationId annotationId) { - if (symbol.TryGetAnnotation(annotationId, out TValue? value)) + if (!symbolToAnnotationStorage.TryGetValue(symbol, out var storage) || !storage.TryGet(symbol, annotationId, out var rawValue)) { + yield break; + } + + if (rawValue is AnnotationList list) { - return value; + // NOTE: These are returned in the reverse order they were added, which means that callers that + // take the first value of a given subtype will get the most recently added value of that subtype + // that the CLI author added to the symbol. + for(int i = list.Count - 1; i >= 0; i--) + { + if (list[i] is TValue expectedTypeValue) { + yield return expectedTypeValue; + } else { + throw new AnnotationCollectionTypeException(annotationId, typeof(TValue), rawValue?.GetType()); + } + } + } + else if (rawValue is TValue singleValue) + { + yield return singleValue; } + else + { + throw new AnnotationCollectionTypeException(annotationId, typeof(TValue), rawValue?.GetType()); + } + } - return default; + // this private subclass ensures we don't cause issues if some annotation has a expected value of type List + class AnnotationList : List + { } } diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs index 313a274ad1..1df81b3033 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationTypeException.cs @@ -20,14 +20,18 @@ public override string Message if (Provider is not null) { return - $"Typed accessor for annotation '${AnnotationId}' expected type '{ExpectedType}' but the annotation provider returned an annotation of type '{ActualType?.ToString() ?? "[null]"}'. " + - $"This may be an authoring error in in the annotation provider '{Provider.GetType()}' or in a typed annotation accessor."; + $"Typed accessor for annotation '${AnnotationId}' expected value of type '{ExpectedType}' but the " + + $"annotation provider returned an value of type '{ActualType?.ToString() ?? "[null]"}'. " + + $"This may be an authoring error in in the annotation provider '{Provider.GetType()}' or in a " + + "typed annotation accessor."; } return - $"Typed accessor for annotation '${AnnotationId}' expected type '{ExpectedType}' but the stored annotation is of type '{ActualType?.ToString() ?? "[null]"}'. " + - $"This may be an authoring error in a typed annotation accessor, or the annotation may have be stored directly with the incorrect type, bypassing the typed accessors."; + $"Typed accessor for annotation '${AnnotationId}' expected value of type '{ExpectedType}' but the stored " + + $"annotation value is of type '{ActualType?.ToString() ?? "[null]"}'. " + + $"This may be an authoring error in a typed annotation accessor, or the annotation may have be stored directly " + + $"with the incorrect type, bypassing the typed accessors."; } } } diff --git a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs index 58602e5f45..5d8d50d21f 100644 --- a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs @@ -68,27 +68,26 @@ public override void Execute(PipelineResult pipelineResult) private void ValidateValue(CliValueSymbol valueSymbol, ValidationContext validationContext) { - var valueConditions = valueSymbol.GetValueConditions(); - if (valueConditions is null) + var valueConditions = valueSymbol.EnumerateValueConditions(); + + var enumerator = valueConditions.GetEnumerator(); + if (!enumerator.MoveNext()) { - return; // nothing to do + // avoid getting the value if there are no conditions + return; } var value = validationContext.GetValue(valueSymbol); var valueResult = validationContext.GetValueResult(valueSymbol); - foreach (var condition in valueConditions) - { - ValidateValueCondition(value, valueSymbol, valueResult, condition, validationContext); - } + + do { + ValidateValueCondition(value, valueSymbol, valueResult, enumerator.Current, validationContext); + } while (enumerator.MoveNext()); } private void ValidateCommand(CliCommandResult commandResult, ValidationContext validationContext) { - var valueConditions = commandResult.Command.GetCommandConditions(); - if (valueConditions is null) - { - return; // nothing to do - } + var valueConditions = commandResult.Command.EnumerateCommandConditions(); foreach (var condition in valueConditions) { diff --git a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs index 1729541d38..f641d08890 100644 --- a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs @@ -1,4 +1,7 @@ -using System.CommandLine.Subsystems.Annotations; +// 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.Subsystems.Annotations; using System.CommandLine.ValueConditions; using System.CommandLine.ValueSources; @@ -58,26 +61,12 @@ public static void SetInclusiveGroup(this CliCommand command, IEnumerable(this TValueSymbol symbol, TValueCondition valueCondition) where TValueSymbol : CliValueSymbol where TValueCondition : ValueCondition - { - if (!symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions)) - { - valueConditions = []; - symbol.SetAnnotation(ValueConditionAnnotations.ValueConditions, valueConditions); - } - valueConditions.Add(valueCondition); - } + => symbol.AddAnnotation(ValueConditionAnnotations.ValueConditions, valueCondition); // TODO: This should not be public if ValueConditions are not public public static void SetValueCondition(this CliCommand symbol, TCommandCondition commandCondition) where TCommandCondition : CommandCondition - { - if (!symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions)) - { - valueConditions = []; - symbol.SetAnnotation(ValueConditionAnnotations.ValueConditions, valueConditions); - } - valueConditions.Add(commandCondition); - } + => symbol.AddAnnotation(ValueConditionAnnotations.ValueConditions, commandCondition); /// /// Gets a list of conditions on an option or argument. @@ -86,10 +75,8 @@ public static void SetValueCondition(this CliCommand symbol, /// The conditions that have been applied to the option or argument. /// // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace - public static List? GetValueConditions(this CliValueSymbol symbol) - => symbol.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) - ? valueConditions - : null; + public static IEnumerable EnumerateValueConditions(this CliValueSymbol symbol) + => symbol.EnumerateAnnotations(ValueConditionAnnotations.ValueConditions); /// /// Gets a list of conditions on an option or argument. @@ -98,10 +85,8 @@ public static void SetValueCondition(this CliCommand symbol, /// The conditions that have been applied to the option or argument. /// // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace - public static List? GetValueConditions(this AnnotationResolver resolver, CliValueSymbol symbol) - => resolver.TryGet>(symbol, ValueConditionAnnotations.ValueConditions, out var valueConditions) - ? valueConditions - : null; + public static IEnumerable EnumerateValueConditions(this AnnotationResolver resolver, CliValueSymbol symbol) + => resolver.Enumerate(symbol, ValueConditionAnnotations.ValueConditions); /// /// Gets a list of conditions on a command. @@ -110,10 +95,8 @@ public static void SetValueCondition(this CliCommand symbol, /// The conditions that have been applied to the command. /// // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace - public static List? GetCommandConditions(this CliCommand command) - => command.TryGetAnnotation>(ValueConditionAnnotations.ValueConditions, out var valueConditions) - ? valueConditions - : null; + public static IEnumerable EnumerateCommandConditions(this CliCommand command) + => command.EnumerateAnnotations(ValueConditionAnnotations.ValueConditions); /// /// Gets a list of conditions on a command. @@ -122,10 +105,8 @@ public static void SetValueCondition(this CliCommand symbol, /// The conditions that have been applied to the command. /// // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace - public static List? GetCommandConditions(this AnnotationResolver resolver, CliCommand command) - => resolver.TryGet>(command, ValueConditionAnnotations.ValueConditions, out var valueConditions) - ? valueConditions - : null; + public static IEnumerable EnumerateCommandConditions(this AnnotationResolver resolver, CliCommand command) + => resolver.Enumerate(command, ValueConditionAnnotations.ValueConditions); /// /// Gets the condition that matches the type, if it exists on this option or argument. @@ -137,9 +118,7 @@ public static void SetValueCondition(this CliCommand symbol, // TODO: Consider removing user facing naming, other than the base type, that is Value or CommandCondition and just use Condition public static TCondition? GetValueCondition(this CliValueSymbol symbol) where TCondition : ValueCondition - => !symbol.TryGetAnnotation(ValueConditionAnnotations.ValueConditions, out List? valueConditions) - ? null - : valueConditions.OfType().LastOrDefault(); + => symbol.EnumerateValueConditions().OfType().LastOrDefault(); /// /// Gets the condition that matches the type, if it exists on this command. @@ -150,9 +129,7 @@ public static void SetValueCondition(this CliCommand symbol, // This method feels useful because it clarifies that last should win and returns one, when only one should be applied public static TCondition? GetCommandCondition(this CliCommand symbol) where TCondition : CommandCondition - => !symbol.TryGetAnnotation(ValueConditionAnnotations.ValueConditions, out List? valueConditions) - ? null - : valueConditions.OfType().LastOrDefault(); + => symbol.EnumerateCommandConditions().OfType().FirstOrDefault(); } From b5e7d057a28f4f1c4fc5d62dae6c708190f3d15a Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 19 Sep 2024 01:40:31 -0400 Subject: [PATCH 148/150] Strongly type SetRange, add GetRange --- .../ValidationSubsystemTests.cs | 30 ++++++++--------- .../ValueConditionAnnotationExtensions.cs | 33 ++++++++++++------- src/System.CommandLine/CliArgument{T}.cs | 4 +-- src/System.CommandLine/CliOption{T}.cs | 2 +- src/System.CommandLine/ICliValueSymbol.cs | 14 ++++++++ .../System.CommandLine.csproj | 1 + 6 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 src/System.CommandLine/ICliValueSymbol.cs diff --git a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs index 3ca4e362c4..91e5622241 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs @@ -11,18 +11,18 @@ namespace System.CommandLine.Subsystems.Tests; public class ValidationSubsystemTests { // Running exactly the same code is important here because missing a step will result in a false positive. Ask me how I know - private CliOption GetOptionWithSimpleRange(T lowerBound, T upperBound) + private CliOption GetOptionWithSimpleRange(string name, T lowerBound, T upperBound) where T : IComparable { - var option = new CliOption("--intOpt"); + var option = new CliOption(name); option.SetRange(lowerBound, upperBound); return option; } - private CliOption GetOptionWithRangeBounds(ValueSource lowerBound, ValueSource upperBound) + private CliOption GetOptionWithRangeBounds(string name, ValueSource lowerBound, ValueSource upperBound) where T : IComparable { - var option = new CliOption("--intOpt"); + var option = new CliOption(name); option.SetRange(lowerBound, upperBound); return option; } @@ -45,7 +45,7 @@ private PipelineResult ExecutedPipelineResultForCommand(CliCommand command, stri [Fact] public void Int_values_in_specified_range_do_not_have_errors() { - var option = GetOptionWithSimpleRange(0, 50); + var option = GetOptionWithSimpleRange("--intOpt", 0, 50); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); @@ -56,7 +56,7 @@ public void Int_values_in_specified_range_do_not_have_errors() [Fact] public void Int_values_above_upper_bound_report_error() { - var option = GetOptionWithSimpleRange(0, 5); + var option = GetOptionWithSimpleRange("--intOpt", 0, 5); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); @@ -69,7 +69,7 @@ public void Int_values_above_upper_bound_report_error() [Fact] public void Int_below_lower_bound_report_error() { - var option = GetOptionWithSimpleRange(0, 5); + var option = GetOptionWithSimpleRange("--intOpt", 0, 5); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt -42"); @@ -82,7 +82,7 @@ public void Int_below_lower_bound_report_error() [Fact] public void Int_values_on_lower_range_bound_do_not_report_error() { - var option = GetOptionWithSimpleRange(42, 50); + var option = GetOptionWithSimpleRange("--intOpt", 42, 50); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); @@ -93,7 +93,7 @@ public void Int_values_on_lower_range_bound_do_not_report_error() [Fact] public void Int_values_on_upper_range_bound_do_not_report_error() { - var option = GetOptionWithSimpleRange(0, 42); + var option = GetOptionWithSimpleRange("--intOpt", 0, 42); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); @@ -104,7 +104,7 @@ public void Int_values_on_upper_range_bound_do_not_report_error() [Fact] public void Values_below_calculated_lower_bound_report_error() { - var option = GetOptionWithRangeBounds(ValueSource.Create(() => (true, 1)), 50); + var option = GetOptionWithRangeBounds("--intOpt", ValueSource.Create(() => (true, 1)), 50); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 0"); @@ -118,7 +118,7 @@ public void Values_below_calculated_lower_bound_report_error() [Fact] public void Values_within_calculated_range_do_not_report_error() { - var option = GetOptionWithRangeBounds(ValueSource.Create(() => (true, 1)), ValueSource.Create(() => (true, 50))); + var option = GetOptionWithRangeBounds("--intOpt", ValueSource.Create(() => (true, 1)), ValueSource.Create(() => (true, 50))); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); @@ -129,7 +129,7 @@ public void Values_within_calculated_range_do_not_report_error() [Fact] public void Values_above_calculated_upper_bound_report_error() { - var option = GetOptionWithRangeBounds(0, ValueSource.Create(() => (true, 40))); + var option = GetOptionWithRangeBounds("--intOpt", 0, ValueSource.Create(() => (true, 40))); var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42"); @@ -143,7 +143,7 @@ public void Values_above_calculated_upper_bound_report_error() public void Values_below_relative_lower_bound_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (true, (int)o + 1)), 50); + var option = GetOptionWithRangeBounds("--intOpt", ValueSource.Create(otherOption, o => (true, (int)o + 1)), 50); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 0 -a 0"); @@ -159,7 +159,7 @@ public void Values_below_relative_lower_bound_report_error() public void Values_within_relative_range_do_not_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (true, (int)o + 1)), ValueSource.Create(otherOption, o => (true, (int)o + 10))); + var option = GetOptionWithRangeBounds("--intOpt", ValueSource.Create(otherOption, o => (true, (int)o + 1)), ValueSource.Create(otherOption, o => (true, (int)o + 10))); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 11 -a 3"); @@ -172,7 +172,7 @@ public void Values_within_relative_range_do_not_report_error() public void Values_above_relative_upper_bound_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds(0, ValueSource.Create(otherOption, o => (true, (int)o + 10))); + var option = GetOptionWithRangeBounds("--intOpt", 0, ValueSource.Create(otherOption, o => (true, (int)o + 10))); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 9 -a -2"); diff --git a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs index f641d08890..dd07228572 100644 --- a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs @@ -13,41 +13,52 @@ namespace System.CommandLine; public static class ValueConditionAnnotationExtensions { /// - /// Set the upper and/or lower bound values of the range. + /// Set upper and/or lower bounds on the range of values that the symbol value may have. /// - /// The type of the bounds. + /// The type of the symbol whose value is bounded by the range. + /// The type of the value that is bounded by the range. /// The option or argument the range applies to. /// The lower bound of the range. /// The upper bound of the range. // TODO: Add RangeBounds // TODO: You should not have to set both...why not nullable? - public static void SetRange(this CliValueSymbol symbol, T lowerBound, T upperBound) - where T : IComparable + public static void SetRange(this TValueSymbol symbol, TValue lowerBound, TValue upperBound) + where TValueSymbol : CliValueSymbol, ICliValueSymbol + where TValue : IComparable { - var range = new Range(lowerBound, upperBound); + var range = new Range(lowerBound, upperBound); symbol.SetValueCondition(range); } /// - /// Set the upper and/or lower bound via ValueSource. Implicit conversions means this - /// generally just works with any . + /// Set upper and/or lower bounds on the range of values that the symbol value may have. + /// Implicit conversions means this generally just works with any . /// - /// The type of the bounds. + /// The type of the symbol whose value is bounded by the range. + /// The type of the value that is bounded by the range. /// The option or argument the range applies to. /// The that is the lower bound of the range. /// The that is the upper bound of the range. // TODO: Add RangeBounds // TODO: You should not have to set both...why not nullable? - public static void SetRange(this CliValueSymbol symbol, ValueSource lowerBound, ValueSource upperBound) - where T : IComparable + public static void SetRange(this TValueSymbol symbol, ValueSource lowerBound, ValueSource upperBound) + where TValueSymbol : CliValueSymbol, ICliValueSymbol + where TValue : IComparable // TODO: You should not have to set both...why not nullable? { - var range = new Range(lowerBound, upperBound); + var range = new Range(lowerBound, upperBound); symbol.SetValueCondition(range); } + /// + /// Get the upper and/or lower bound of the symbol's value. + /// + /// The option or argument the range applies to. + public static ValueConditions.Range? GetRange(this CliValueSymbol symbol) + => symbol.GetValueCondition(); + /// /// Indicates that there is an inclusive group of options and arguments for the command. All /// members of an inclusive must be present, or none can be present. diff --git a/src/System.CommandLine/CliArgument{T}.cs b/src/System.CommandLine/CliArgument{T}.cs index 14015b7ac5..f21c2db0d5 100644 --- a/src/System.CommandLine/CliArgument{T}.cs +++ b/src/System.CommandLine/CliArgument{T}.cs @@ -4,12 +4,12 @@ using System.Collections.Generic; using System.CommandLine.Parsing; using System.Diagnostics.CodeAnalysis; -using System.IO; namespace System.CommandLine { + /// - public class CliArgument : CliArgument + public class CliArgument : CliArgument, ICliValueSymbol { // TODO: custom parser /* diff --git a/src/System.CommandLine/CliOption{T}.cs b/src/System.CommandLine/CliOption{T}.cs index f2ca874d1a..21db11cb97 100644 --- a/src/System.CommandLine/CliOption{T}.cs +++ b/src/System.CommandLine/CliOption{T}.cs @@ -7,7 +7,7 @@ namespace System.CommandLine { /// /// The that the option's arguments are expected to be parsed as. - public class CliOption : CliOption + public class CliOption : CliOption, ICliValueSymbol { // TODO: do not expose private fields internal readonly CliArgument _argument; diff --git a/src/System.CommandLine/ICliValueSymbol.cs b/src/System.CommandLine/ICliValueSymbol.cs new file mode 100644 index 0000000000..d59c3b1975 --- /dev/null +++ b/src/System.CommandLine/ICliValueSymbol.cs @@ -0,0 +1,14 @@ +// 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; + +/// +/// This is applied to and , and allows +/// allows methods with a argument to apply constraints based on the +/// value type. +/// +/// The value type +public interface ICliValueSymbol +{ +} diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index cfb211a276..bee122292b 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -27,6 +27,7 @@ + From 1c290f62d456ebd9fd8f1651ff21903d841a0cd5 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Fri, 20 Sep 2024 17:43:22 -0400 Subject: [PATCH 149/150] Use the annotation resolver in ValueProvider --- .../ValueAnnotationExtensions.cs | 9 ++++++--- src/System.CommandLine.Subsystems/ValueProvider.cs | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs index 676bcc9b7d..68d3d6324e 100644 --- a/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueAnnotationExtensions.cs @@ -17,9 +17,12 @@ public static class ValueAnnotationExtensions /// The option /// The option's default value annotation if any, otherwise /// - /// This is intended to be called by CLI authors. Subsystems should instead call , - /// which calculates the actual default value, based on the default value annotation and default value calculation, - /// whether directly stored on the symbol or from the subsystem's . + /// This is intended to be called by CLI authors inspecting the default value source they have + /// associated directly with the . + /// + /// Subsystems should instead use , which caches calculated + /// values and respects dynamic/lazy annotations from the . + /// /// public static bool TryGetDefaultValueSource(this CliValueSymbol valueSymbol, [NotNullWhen(true)] out ValueSource? defaultValueSource) => valueSymbol.TryGetAnnotation(ValueAnnotations.DefaultValueSource, out defaultValueSource); diff --git a/src/System.CommandLine.Subsystems/ValueProvider.cs b/src/System.CommandLine.Subsystems/ValueProvider.cs index f51a1edb63..adb6b9c90e 100644 --- a/src/System.CommandLine.Subsystems/ValueProvider.cs +++ b/src/System.CommandLine.Subsystems/ValueProvider.cs @@ -1,6 +1,7 @@ // 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.Subsystems.Annotations; using System.CommandLine.ValueSources; namespace System.CommandLine; @@ -48,7 +49,7 @@ private bool TryGetValue(CliSymbol symbol, out T? value) { return UseValue(valueSymbol, valueResult.GetValue()); } - if (valueSymbol.TryGetDefaultValueSource(out ValueSource? defaultValueSource)) + if (pipelineResult.Annotations.TryGet(valueSymbol, ValueAnnotations.DefaultValueSource, out ValueSource? defaultValueSource)) { if (defaultValueSource is not ValueSource typedDefaultValueSource) { From ae8534928dd4c44ab22ab39afd79fe854ea56700 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 24 Sep 2024 15:35:29 -0400 Subject: [PATCH 150/150] Address PR feedback --- .../ValidationSubsystemTests.cs | 8 ++++--- .../Annotations/AnnotationResolver.cs | 4 ++-- .../AnnotationStorageExtensions.cs | 2 +- .../ValidationSubsystem.cs | 6 +++++- .../ValueConditionAnnotationExtensions.cs | 21 +++---------------- .../RelativeToSymbolValueSource.cs | 4 ++-- .../ValueSources/ValueSource.cs | 2 +- src/System.CommandLine/ICliValueSymbol.cs | 2 +- 8 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs index 91e5622241..e9d2e30833 100644 --- a/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs +++ b/src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs @@ -143,7 +143,7 @@ public void Values_above_calculated_upper_bound_report_error() public void Values_below_relative_lower_bound_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds("--intOpt", ValueSource.Create(otherOption, o => (true, (int)o + 1)), 50); + var option = GetOptionWithRangeBounds("--intOpt", ValueSource.Create(otherOption, o => (true, o + 1)), 50); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 0 -a 0"); @@ -159,7 +159,9 @@ public void Values_below_relative_lower_bound_report_error() public void Values_within_relative_range_do_not_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds("--intOpt", ValueSource.Create(otherOption, o => (true, (int)o + 1)), ValueSource.Create(otherOption, o => (true, (int)o + 10))); + var option = GetOptionWithRangeBounds("--intOpt", + ValueSource.Create(otherOption, o => (true, o + 1)), + ValueSource.Create(otherOption, o => (true, o + 10))); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 11 -a 3"); @@ -172,7 +174,7 @@ public void Values_within_relative_range_do_not_report_error() public void Values_above_relative_upper_bound_report_error() { var otherOption = new CliOption("-a"); - var option = GetOptionWithRangeBounds("--intOpt", 0, ValueSource.Create(otherOption, o => (true, (int)o + 10))); + var option = GetOptionWithRangeBounds("--intOpt", 0, ValueSource.Create(otherOption, o => (true, o + 10))); var command = new CliCommand("cmd") { option, otherOption }; var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 9 -a -2"); diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs index f49ce86cad..0a16cd40a1 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationResolver.cs @@ -41,8 +41,8 @@ public AnnotationResolver(PipelineResult pipelineResult) /// True if successful /// /// This is intended for use by developers defining custom annotation IDs. Anyone defining an annotation - /// ID should also define an accessor extension method on extension method - /// on that subsystem authors can use to access the annotation value, such as + /// ID should also define an accessor extension method on + /// that subsystem authors can use to access the annotation value, such as /// . /// /// If the annotation value does not have a single expected type for this symbol, use the diff --git a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs index b62f205257..cddca63bdd 100644 --- a/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs +++ b/src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationStorageExtensions.cs @@ -103,7 +103,7 @@ public static TSymbol WithAnnotation(this TSymbol symbol, AnnotationId /// This is intended to be called by the implementation of specialized ID-specific accessors for CLI authors such as . /// /// Subsystems should not call this directly, as it does not account for values from the pipeline's . - /// They should instead access annotations from the see cref="Pipeline.Annotations"/> property using + /// They should instead access annotations from the property using /// or an ID-specific /// extension method such as . /// diff --git a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs index 5d8d50d21f..00e44e9971 100644 --- a/src/System.CommandLine.Subsystems/ValidationSubsystem.cs +++ b/src/System.CommandLine.Subsystems/ValidationSubsystem.cs @@ -70,10 +70,11 @@ private void ValidateValue(CliValueSymbol valueSymbol, ValidationContext validat { var valueConditions = valueSymbol.EnumerateValueConditions(); + // manually implement the foreach so we can efficiently skip getting the + // value if there are no conditions var enumerator = valueConditions.GetEnumerator(); if (!enumerator.MoveNext()) { - // avoid getting the value if there are no conditions return; } @@ -89,6 +90,9 @@ private void ValidateCommand(CliCommandResult commandResult, ValidationContext v { var valueConditions = commandResult.Command.EnumerateCommandConditions(); + // unlike ValidateValue we do not need to manually implement the foreach + // to skip unnecessary work, as there is no additional work to be before + // calling command validators foreach (var condition in valueConditions) { ValidateCommandCondition(commandResult, condition, validationContext); diff --git a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs index dd07228572..77a9e65b8c 100644 --- a/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs +++ b/src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs @@ -20,8 +20,7 @@ public static class ValueConditionAnnotationExtensions /// The option or argument the range applies to. /// The lower bound of the range. /// The upper bound of the range. - // TODO: Add RangeBounds - // TODO: You should not have to set both...why not nullable? + // TODO: can we eliminate this overload and just reply on the implicit cast to ValueSource? public static void SetRange(this TValueSymbol symbol, TValue lowerBound, TValue upperBound) where TValueSymbol : CliValueSymbol, ICliValueSymbol where TValue : IComparable @@ -40,12 +39,9 @@ public static void SetRange(this TValueSymbol symbol, TVal /// The option or argument the range applies to. /// The that is the lower bound of the range. /// The that is the upper bound of the range. - // TODO: Add RangeBounds - // TODO: You should not have to set both...why not nullable? - public static void SetRange(this TValueSymbol symbol, ValueSource lowerBound, ValueSource upperBound) + public static void SetRange(this TValueSymbol symbol, ValueSource? lowerBound, ValueSource? upperBound) where TValueSymbol : CliValueSymbol, ICliValueSymbol where TValue : IComparable - // TODO: You should not have to set both...why not nullable? { var range = new Range(lowerBound, upperBound); @@ -68,13 +64,11 @@ public static void SetRange(this TValueSymbol symbol, Valu public static void SetInclusiveGroup(this CliCommand command, IEnumerable group) => command.SetValueCondition(new InclusiveGroup(group)); - // TODO: This should not be public if ValueConditions are not public public static void SetValueCondition(this TValueSymbol symbol, TValueCondition valueCondition) where TValueSymbol : CliValueSymbol where TValueCondition : ValueCondition => symbol.AddAnnotation(ValueConditionAnnotations.ValueConditions, valueCondition); - // TODO: This should not be public if ValueConditions are not public public static void SetValueCondition(this CliCommand symbol, TCommandCondition commandCondition) where TCommandCondition : CommandCondition => symbol.AddAnnotation(ValueConditionAnnotations.ValueConditions, commandCondition); @@ -84,8 +78,6 @@ public static void SetValueCondition(this CliCommand symbol, /// /// The option or argument to get the conditions for. /// The conditions that have been applied to the option or argument. - /// - // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace public static IEnumerable EnumerateValueConditions(this CliValueSymbol symbol) => symbol.EnumerateAnnotations(ValueConditionAnnotations.ValueConditions); @@ -94,8 +86,6 @@ public static IEnumerable EnumerateValueConditions(this CliValue /// /// The option or argument to get the conditions for. /// The conditions that have been applied to the option or argument. - /// - // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace public static IEnumerable EnumerateValueConditions(this AnnotationResolver resolver, CliValueSymbol symbol) => resolver.Enumerate(symbol, ValueConditionAnnotations.ValueConditions); @@ -104,8 +94,6 @@ public static IEnumerable EnumerateValueConditions(this Annotati /// /// The command to get the conditions for. /// The conditions that have been applied to the command. - /// - // TODO: This is public because it will be used by other subsystems we might not own. It could be an extension method the subsystem namespace public static IEnumerable EnumerateCommandConditions(this CliCommand command) => command.EnumerateAnnotations(ValueConditionAnnotations.ValueConditions); @@ -125,11 +113,9 @@ public static IEnumerable EnumerateCommandConditions(this Anno /// The type of condition to return. /// The option or argument that may contain the condition. /// The condition if it exists on the option or argument, otherwise null. - // This method feels useful because it clarifies that last should win and returns one, when only one should be applied - // TODO: Consider removing user facing naming, other than the base type, that is Value or CommandCondition and just use Condition public static TCondition? GetValueCondition(this CliValueSymbol symbol) where TCondition : ValueCondition - => symbol.EnumerateValueConditions().OfType().LastOrDefault(); + => symbol.EnumerateValueConditions().OfType().FirstOrDefault(); /// /// Gets the condition that matches the type, if it exists on this command. @@ -137,7 +123,6 @@ public static IEnumerable EnumerateCommandConditions(this Anno /// The type of condition to return. /// The command that may contain the condition. /// The condition if it exists on the command, otherwise null. - // This method feels useful because it clarifies that last should win and returns one, when only one should be applied public static TCondition? GetCommandCondition(this CliCommand symbol) where TCondition : CommandCondition => symbol.EnumerateCommandConditions().OfType().FirstOrDefault(); diff --git a/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs index a53e13c573..ba48870130 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/RelativeToSymbolValueSource.cs @@ -17,7 +17,7 @@ public sealed class RelativeToSymbolValueSource internal RelativeToSymbolValueSource( CliValueSymbol otherSymbol, bool onlyUserEnteredValues = false, - Func? calculation = null, + Func? calculation = null, string? description = null) { OtherSymbol = otherSymbol; @@ -29,7 +29,7 @@ internal RelativeToSymbolValueSource( public override string? Description { get; } public CliValueSymbol OtherSymbol { get; } public bool OnlyUserEnteredValues { get; } - public Func? Calculation { get; } + public Func? Calculation { get; } /// public override bool TryGetTypedValue(PipelineResult pipelineResult, out T? value) diff --git a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs index 43098896ed..18ef7a97b6 100644 --- a/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs +++ b/src/System.CommandLine.Subsystems/ValueSources/ValueSource.cs @@ -31,7 +31,7 @@ public static ValueSource Create(Func<(bool success, T? value)> calculatio => new CalculatedValueSource(calculation, description); public static ValueSource Create(CliValueSymbol otherSymbol, - Func? calculation = null, + Func? calculation = null, bool userEnteredValueOnly = false, string? description = null) => new RelativeToSymbolValueSource(otherSymbol, userEnteredValueOnly, calculation, description); diff --git a/src/System.CommandLine/ICliValueSymbol.cs b/src/System.CommandLine/ICliValueSymbol.cs index d59c3b1975..d4d7dbdddf 100644 --- a/src/System.CommandLine/ICliValueSymbol.cs +++ b/src/System.CommandLine/ICliValueSymbol.cs @@ -4,7 +4,7 @@ namespace System.CommandLine; /// -/// This is applied to and , and allows +/// This is implemented only by and , and allows /// allows methods with a argument to apply constraints based on the /// value type. ///