diff --git a/samples/RenderingPlayground/Program.cs b/samples/RenderingPlayground/Program.cs index a9593a03d0..f957db3084 100644 --- a/samples/RenderingPlayground/Program.cs +++ b/samples/RenderingPlayground/Program.cs @@ -54,7 +54,7 @@ public static void Main( var consoleRenderer = new ConsoleRenderer( console, - mode: invocationContext.BindingContext.OutputMode(), + mode: OutputMode.Auto, resetAfterRender: true); switch (sample) diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_Hosting_api_is_not_changed.approved.txt b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_Hosting_api_is_not_changed.approved.txt index fb206743ff..4772b4bfa0 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_Hosting_api_is_not_changed.approved.txt +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_Hosting_api_is_not_changed.approved.txt @@ -1,6 +1,6 @@ System.CommandLine.Hosting public static class DirectiveConfigurationExtensions - public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddCommandLineDirectives(this Microsoft.Extensions.Configuration.IConfigurationBuilder config, System.CommandLine.ParseResult commandline, System.String name) + public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddCommandLineDirectives(this Microsoft.Extensions.Configuration.IConfigurationBuilder config, System.CommandLine.ParseResult commandline, System.CommandLine.Directive directive) public static class HostingExtensions public static OptionsBuilder BindCommandLine(this OptionsBuilder optionsBuilder) public static Microsoft.Extensions.Hosting.IHost GetHost(this System.CommandLine.Invocation.InvocationContext invocationContext) diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt index 02b7a38200..9688ad3dcf 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt @@ -58,11 +58,11 @@ System.CommandLine public class CommandLineBuilder .ctor(Command rootCommand) public Command Command { get; } + public System.Collections.Generic.List Directives { get; } public CommandLineBuilder AddMiddleware(System.CommandLine.Invocation.InvocationMiddleware middleware, System.CommandLine.Invocation.MiddlewareOrder order = Default) public CommandLineBuilder AddMiddleware(System.Action onInvoke, System.CommandLine.Invocation.MiddlewareOrder order = Default) public CommandLineConfiguration Build() public CommandLineBuilder CancelOnProcessTermination(System.Nullable timeout = null) - public CommandLineBuilder EnableDirectives(System.Boolean value = True) public CommandLineBuilder EnablePosixBundling(System.Boolean value = True) public CommandLineBuilder RegisterWithDotnetSuggest() public CommandLineBuilder UseDefaults() @@ -81,8 +81,8 @@ System.CommandLine public CommandLineBuilder UseVersionOption(System.String name, System.String[] aliases) public class CommandLineConfiguration public static CommandLineBuilder CreateBuilder(Command rootCommand) - .ctor(Command command, System.Boolean enablePosixBundling = True, System.Boolean enableDirectives = True, System.Boolean enableTokenReplacement = True, System.Collections.Generic.IReadOnlyList middlewarePipeline = null, System.Func helpBuilderFactory = null, System.CommandLine.Parsing.TryReplaceToken tokenReplacer = null) - public System.Boolean EnableDirectives { get; } + .ctor(Command command, System.Boolean enablePosixBundling = True, System.Boolean enableTokenReplacement = True, System.Collections.Generic.IReadOnlyList middlewarePipeline = null, System.Func helpBuilderFactory = null, System.CommandLine.Parsing.TryReplaceToken tokenReplacer = null) + public System.Collections.Generic.IReadOnlyList Directives { get; } public System.Boolean EnablePosixBundling { get; } public System.Boolean EnableTokenReplacement { get; } public Command RootCommand { get; } @@ -101,11 +101,13 @@ System.CommandLine public static class ConsoleExtensions public static System.Void Write(this IConsole console, System.String value) public static System.Void WriteLine(this IConsole console, System.String value) - public class DirectiveCollection, System.Collections.Generic.IEnumerable>>, System.Collections.IEnumerable + public class Directive : Symbol + .ctor(System.String name, System.Action syncHandler = null, System.Func asyncHandler = null) + public System.Collections.Generic.IEnumerable GetCompletions(System.CommandLine.Completions.CompletionContext context) + public System.Void SetAsynchronousHandler(System.Func handler) + public System.Void SetSynchronousHandler(System.Action handler) + public class EnvironmentVariablesDirective : Directive .ctor() - public System.Boolean Contains(System.String name) - public System.Collections.Generic.IEnumerator>> GetEnumerator() - public System.Boolean TryGetValues(System.String name, ref System.Collections.Generic.IReadOnlyList values) public static class Handler public static System.Void SetHandler(this Command command, System.Action handle) public static System.Void SetHandler(this Command command, System.Action handle) @@ -154,10 +156,11 @@ System.CommandLine public static Option AcceptExistingOnly(this Option option) public static Option AcceptExistingOnly(this Option option) public static Option AcceptExistingOnly(this Option option) + public class ParseDirective : Directive + .ctor(System.Int32 errorExitCode = 1) public class ParseResult public System.CommandLine.Parsing.CommandResult CommandResult { get; } public CommandLineConfiguration Configuration { get; } - public System.Collections.Generic.IReadOnlyDictionary> Directives { get; } public System.Collections.Generic.IReadOnlyList Errors { get; } public System.CommandLine.Parsing.CommandResult RootCommandResult { get; } public System.Collections.Generic.IReadOnlyList Tokens { get; } @@ -165,6 +168,7 @@ System.CommandLine public System.CommandLine.Parsing.ArgumentResult FindResultFor(Argument argument) public System.CommandLine.Parsing.CommandResult FindResultFor(Command command) public System.CommandLine.Parsing.OptionResult FindResultFor(Option option) + public System.CommandLine.Parsing.DirectiveResult FindResultFor(Directive directive) public System.CommandLine.Parsing.SymbolResult FindResultFor(Symbol symbol) public System.CommandLine.Completions.CompletionContext GetCompletionContext() public System.Collections.Generic.IEnumerable GetCompletions(System.Nullable position = null) @@ -177,6 +181,8 @@ System.CommandLine public static System.String ExecutableName { get; } public static System.String ExecutablePath { get; } .ctor(System.String description = ) + public class SuggestDirective : Directive + .ctor() public abstract class Symbol public System.String Description { get; set; } public System.Boolean IsHidden { get; set; } @@ -337,6 +343,10 @@ System.CommandLine.Parsing public System.CommandLine.Command Command { get; } public Token Token { get; } public System.String ToString() + public class DirectiveResult : SymbolResult + public System.CommandLine.Directive Directive { get; } + public Token Token { get; } + public System.Collections.Generic.IReadOnlyList Values { get; } public class OptionResult : SymbolResult public System.Boolean IsImplicit { get; } public System.CommandLine.Option Option { get; } @@ -360,6 +370,7 @@ System.CommandLine.Parsing public ArgumentResult FindResultFor(System.CommandLine.Argument argument) public CommandResult FindResultFor(System.CommandLine.Command command) public OptionResult FindResultFor(System.CommandLine.Option option) + public DirectiveResult FindResultFor(System.CommandLine.Directive directive) public T GetValue(Argument argument) public T GetValue(Option option) public class Token, System.IEquatable diff --git a/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_ParseResult.cs b/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_ParseResult.cs index 381cd5be9e..222fbac158 100644 --- a/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_ParseResult.cs +++ b/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_ParseResult.cs @@ -43,8 +43,8 @@ public IEnumerable GenerateTestParseResults() [Benchmark] [ArgumentsSource(nameof(GenerateTestInputs))] - public IReadOnlyDictionary> ParseResult_Directives(string input) - => _configuration.RootCommand.Parse(input, _configuration).Directives; + public ParseResult ParseResult_Directives(string input) + => _configuration.RootCommand.Parse(input, _configuration); [Benchmark] [ArgumentsSource(nameof(GenerateTestParseResults))] diff --git a/src/System.CommandLine.Hosting/DirectiveConfigurationExtensions.cs b/src/System.CommandLine.Hosting/DirectiveConfigurationExtensions.cs index a09f629f24..4a9238af71 100644 --- a/src/System.CommandLine.Hosting/DirectiveConfigurationExtensions.cs +++ b/src/System.CommandLine.Hosting/DirectiveConfigurationExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.CommandLine.Parsing; using System.Linq; using Microsoft.Extensions.Configuration; @@ -9,18 +10,21 @@ public static class DirectiveConfigurationExtensions { public static IConfigurationBuilder AddCommandLineDirectives( this IConfigurationBuilder config, ParseResult commandline, - string name) + Directive directive) { if (commandline is null) throw new ArgumentNullException(nameof(commandline)); - if (name is null) - throw new ArgumentNullException(nameof(name)); + if (directive is null) + throw new ArgumentNullException(nameof(directive)); - if (!commandline.Directives.TryGetValue(name, out var directives)) + if (commandline.FindResultFor(directive) is not DirectiveResult result + || result.Values.Count == 0) + { return config; + } var kvpSeparator = new[] { '=' }; - return config.AddInMemoryCollection(directives.Select(s => + return config.AddInMemoryCollection(result.Values.Select(s => { var parts = s.Split(kvpSeparator, count: 2); var key = parts[0]; diff --git a/src/System.CommandLine.Hosting/HostingExtensions.cs b/src/System.CommandLine.Hosting/HostingExtensions.cs index 1a4eb4637a..93573e7332 100644 --- a/src/System.CommandLine.Hosting/HostingExtensions.cs +++ b/src/System.CommandLine.Hosting/HostingExtensions.cs @@ -13,12 +13,14 @@ namespace System.CommandLine.Hosting { public static class HostingExtensions { - private const string ConfigurationDirectiveName = "config"; - public static CommandLineBuilder UseHost(this CommandLineBuilder builder, Func hostBuilderFactory, - Action configureHost = null) => - builder.AddMiddleware(async (invocation, cancellationToken, next) => + Action configureHost = null) + { + Directive configurationDirective = new("config"); + builder.Directives.Add(configurationDirective); + + return builder.AddMiddleware(async (invocation, cancellationToken, next) => { var argsRemaining = invocation.ParseResult.UnmatchedTokens.ToArray(); var hostBuilder = hostBuilderFactory?.Invoke(argsRemaining) @@ -27,7 +29,7 @@ public static CommandLineBuilder UseHost(this CommandLineBuilder builder, hostBuilder.ConfigureHostConfiguration(config => { - config.AddCommandLineDirectives(invocation.ParseResult, ConfigurationDirectiveName); + config.AddCommandLineDirectives(invocation.ParseResult, configurationDirective); }); hostBuilder.ConfigureServices(services => { @@ -50,6 +52,7 @@ public static CommandLineBuilder UseHost(this CommandLineBuilder builder, await host.StopAsync(cancellationToken); }); + } public static CommandLineBuilder UseHost(this CommandLineBuilder builder, Action configureHost = null diff --git a/src/System.CommandLine.Rendering/CommandLineBuilderExtensions.cs b/src/System.CommandLine.Rendering/CommandLineBuilderExtensions.cs index 185d24c9f8..c8e1b95aeb 100644 --- a/src/System.CommandLine.Rendering/CommandLineBuilderExtensions.cs +++ b/src/System.CommandLine.Rendering/CommandLineBuilderExtensions.cs @@ -1,8 +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.Binding; -using System.Linq; +using System.CommandLine.Parsing; namespace System.CommandLine.Rendering { @@ -11,13 +10,18 @@ public static class CommandLineBuilderExtensions public static CommandLineBuilder UseAnsiTerminalWhenAvailable( this CommandLineBuilder builder) { + Directive enableVtDirective = new ("enable-vt"); + Directive outputDirective = new ("output"); + builder.Directives.Add(enableVtDirective); + builder.Directives.Add(outputDirective); + builder.AddMiddleware(context => { var console = context.Console; var terminal = console.GetTerminal( - PreferVirtualTerminal(context.BindingContext), - OutputMode(context.BindingContext)); + PreferVirtualTerminal(context.ParseResult, enableVtDirective), + OutputMode(context.ParseResult, outputDirective)); context.Console = terminal ?? console; }); @@ -25,16 +29,14 @@ public static CommandLineBuilder UseAnsiTerminalWhenAvailable( return builder; } - internal static bool PreferVirtualTerminal( - this BindingContext context) + private static bool PreferVirtualTerminal(ParseResult parseResult, Directive enableVtDirective) { - if (context.ParseResult.Directives.TryGetValue( - "enable-vt", - out var trueOrFalse)) + if (parseResult.FindResultFor(enableVtDirective) is DirectiveResult result + && result.Values.Count == 1) { - if (bool.TryParse( - trueOrFalse.FirstOrDefault(), - out var pvt)) + string trueOrFalse = result.Values[0]; + + if (bool.TryParse(trueOrFalse, out var pvt)) { return pvt; } @@ -43,15 +45,11 @@ internal static bool PreferVirtualTerminal( return true; } - public static OutputMode OutputMode(this BindingContext context) + private static OutputMode OutputMode(ParseResult parseResult, Directive outputDirective) { - if (context.ParseResult.Directives.TryGetValue( - "output", - out var modeString) && - Enum.TryParse( - modeString.FirstOrDefault(), - true, - out var mode)) + if (parseResult.FindResultFor(outputDirective) is DirectiveResult result + && result.Values.Count == 1 + && Enum.TryParse(result.Values[0], true, out var mode)) { return mode; } diff --git a/src/System.CommandLine.Tests/DirectiveTests.cs b/src/System.CommandLine.Tests/DirectiveTests.cs index 76d5437ed1..938985e3c2 100644 --- a/src/System.CommandLine.Tests/DirectiveTests.cs +++ b/src/System.CommandLine.Tests/DirectiveTests.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 FluentAssertions; using Xunit; @@ -12,55 +11,50 @@ namespace System.CommandLine.Tests public class DirectiveTests { [Fact] - public void Directives_should_not_be_considered_as_unmatched_tokens() + public void Directives_should_be_considered_as_unmatched_tokens_when_they_are_not_matched() { - var option = new Option("-y"); + Directive directive = new("parse"); - var result = new RootCommand { option }.Parse($"{RootCommand.ExecutableName} [parse] -y"); + ParseResult result = Parse(new Option("-y"), directive, $"{RootCommand.ExecutableName} [nonExisting] -y"); - result.UnmatchedTokens.Should().BeEmpty(); + result.UnmatchedTokens.Should().ContainSingle("[nonExisting]"); } [Fact] public void Raw_tokens_still_hold_directives() { - var option = new Option("-y"); + Directive directive = new ("parse"); - var result = new RootCommand { option }.Parse("[parse] -y"); + ParseResult result = Parse(new Option("-y"), directive, "[parse] -y"); - result.Directives.ContainsKey("parse").Should().BeTrue(); + result.FindResultFor(directive).Should().NotBeNull(); result.Tokens.Should().Contain(t => t.Value == "[parse]"); } - [Fact] - public void Directives_should_parse_into_the_directives_collection() - { - var option = new Option("-y"); - - var result = new RootCommand { option }.Parse("[parse] -y"); - - result.Directives.ContainsKey("parse").Should().BeTrue(); - } - [Fact] public void Multiple_directives_are_allowed() { - var option = new Option("-y"); + RootCommand root = new() { new Option("-y") }; + Directive parseDirective = new ("parse"); + Directive suggestDirective = new ("suggest"); + CommandLineBuilder builder = new(root); + builder.Directives.Add(parseDirective); + builder.Directives.Add(suggestDirective); - var result = new RootCommand { option }.Parse("[parse] [suggest] -y"); + var result = root.Parse("[parse] [suggest] -y", builder.Build()); - result.Directives.ContainsKey("parse").Should().BeTrue(); - result.Directives.ContainsKey("suggest").Should().BeTrue(); + result.FindResultFor(parseDirective).Should().NotBeNull(); + result.FindResultFor(suggestDirective).Should().NotBeNull(); } [Fact] public void Directives_must_be_the_first_argument() { - var option = new Option("-y"); + Directive directive = new("parse"); - var result = new RootCommand { option }.Parse("-y [suggest]"); + ParseResult result = Parse(new Option("-y"), directive, "-y [parse]"); - result.Directives.Should().BeEmpty(); + result.FindResultFor(directive).Should().BeNull(); } [Theory] @@ -68,27 +62,25 @@ public void Directives_must_be_the_first_argument() [InlineData("[key:value:more]", "key", "value:more")] [InlineData("[key:]", "key", "")] public void Directives_can_have_a_value_which_is_everything_after_the_first_colon( - string directive, - string expectedKey, + string wholeText, + string key, string expectedValue) { - var option = new Option("-y"); + Directive directive = new(key); - var result = new RootCommand { option }.Parse($"{directive} -y"); + ParseResult result = Parse(new Option("-y"), directive, $"{wholeText} -y"); - result.Directives.TryGetValue(expectedKey, out var values).Should().BeTrue(); - values.Should().BeEquivalentTo(expectedValue); + result.FindResultFor(directive).Values.Single().Should().Be(expectedValue); } [Fact] - public void Directives_without_a_value_specified_have_a_value_of_empty_string() + public void Directives_without_a_value_specified_have_no_values() { - var option = new Option("-y"); + Directive directive = new("parse"); - var result = new RootCommand { option }.Parse("[parse] -y"); + ParseResult result = Parse(new Option("-y"), directive, "[parse] -y"); - result.Directives.TryGetValue("parse", out var values).Should().BeTrue(); - values.Should().BeEmpty(); + result.FindResultFor(directive).Values.Should().BeEmpty(); } [Theory] @@ -96,68 +88,47 @@ public void Directives_without_a_value_specified_have_a_value_of_empty_string() [InlineData("[:value]")] public void Directives_must_have_a_non_empty_key(string directive) { - var option = new Option("-a"); + Option option = new ("-a"); + RootCommand root = new () { option }; - var result = new RootCommand { option }.Parse($"{directive} -a"); + var result = root.Parse($"{directive} -a"); - result.Directives.Should().BeEmpty(); result.UnmatchedTokens.Should().Contain(directive); } [Theory] - [InlineData("[par se]")] - [InlineData("[ parse]")] - [InlineData("[parse ]")] - public void Directives_cannot_contain_spaces(object value) + [InlineData("[par se]", "[par", "se]")] + [InlineData("[ parse]", "[", "parse]")] + [InlineData("[parse ]", "[parse", "]")] + public void Directives_cannot_contain_spaces(string value, string firstUnmatchedToken, string secondUnmatchedToken) { - var option = new Option("-a"); + Action create = () => new Directive(value); + create.Should().Throw(); - var result = new RootCommand { option }.Parse($"{value} -a"); + Directive directive = new("parse"); + ParseResult result = Parse(new Option("-y"), directive, $"{value} -y"); + result.FindResultFor(directive).Should().BeNull(); - result.Directives.Should().BeEmpty(); + result.UnmatchedTokens.Should().BeEquivalentTo(firstUnmatchedToken, secondUnmatchedToken); } [Fact] public void When_a_directive_is_specified_more_than_once_then_its_values_are_aggregated() { - var option = new Option("-a"); + Directive directive = new("directive"); - var result = new RootCommand { option }.Parse("[directive:one] [directive:two] -a"); + ParseResult result = Parse(new Option("-a"), directive, "[directive:one] [directive:two] -a"); - result.Directives.TryGetValue("directive", out var values).Should().BeTrue(); - values.Should().BeEquivalentTo("one", "two"); + result.FindResultFor(directive).Values.Should().BeEquivalentTo("one", "two"); } - [Fact] - public void Directive_count_is_based_on_distinct_instances_of_directive_name() + private static ParseResult Parse(Option option, Directive directive, string commandLine) { - var command = new RootCommand(); - - var result = command.Parse("[one] [two] [one:a] [one:b]"); - - result.Directives.Should().HaveCount(2); - } + RootCommand root = new() { option }; + CommandLineBuilder builder = new(root); + builder.Directives.Add(directive); - [Fact] - public void Directives_can_be_disabled() - { - RootCommand rootCommand = new () - { - new Argument>("args") - }; - var configuration = - new CommandLineConfiguration( - rootCommand, - enableDirectives: false); - - var result = rootCommand.Parse("[hello]", configuration); - - result.Directives.Count().Should().Be(0); - result.CommandResult - .Tokens - .Select(t => t.Value) - .Should() - .BeEquivalentTo("[hello]"); + return root.Parse(commandLine, builder.Build()); } } -} +} \ No newline at end of file diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 5a7ddd7e08..824fb59afe 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -453,12 +453,14 @@ public void Relative_order_of_arguments_and_options_within_a_command_does_not_ma .BeEquivalentTo( result2, x => x.IgnoringCyclicReferences() - .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.Internal))); + .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.Internal)) + .Excluding(y => y.WhichGetterHas(CSharpAccessModifier.PrivateProtected))); } [Theory] diff --git a/src/System.CommandLine/Builder/CommandLineBuilder.cs b/src/System.CommandLine/Builder/CommandLineBuilder.cs index 83b7e617c6..1941badfce 100644 --- a/src/System.CommandLine/Builder/CommandLineBuilder.cs +++ b/src/System.CommandLine/Builder/CommandLineBuilder.cs @@ -23,18 +23,6 @@ public partial class CommandLineBuilder /// internal int? ParseErrorReportingExitCode; - /// - internal bool EnableDirectivesFlag = true; - - /// - internal bool EnableEnvironmentVariableDirective; - - /// - internal int? ParseDirectiveExitCode; - - /// - internal bool EnableSuggestDirective; - /// internal int MaxLevenshteinDistance; @@ -91,18 +79,20 @@ HelpBuilder CreateHelpBuilder(BindingContext bindingContext) internal TryReplaceToken? TokenReplacer; + public List Directives => _directives ??= new (); + + private List? _directives; + + /// /// Creates a parser based on the configuration of the command line builder. /// public CommandLineConfiguration Build() => new ( Command, + _directives, enablePosixBundling: EnablePosixBundlingFlag, - enableDirectives: EnableDirectivesFlag, enableTokenReplacement: EnableTokenReplacement, - enableEnvironmentVariableDirective: EnableEnvironmentVariableDirective, - parseDirectiveExitCode: ParseDirectiveExitCode, - enableSuggestDirective: EnableSuggestDirective, parseErrorReportingExitCode: ParseErrorReportingExitCode, maxLevenshteinDistance: MaxLevenshteinDistance, exceptionHandler: ExceptionHandler, diff --git a/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs b/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs index 807e9f5cc2..211a97d09d 100644 --- a/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs +++ b/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs @@ -32,20 +32,6 @@ public CommandLineBuilder CancelOnProcessTermination(TimeSpan? timeout = null) return this; } - /// - /// Enables the parser to recognize command line directives. - /// - /// to enable directives. to parse directive-like tokens in the same way as any other token. - /// The reference to this instance. - /// Command-line directives - /// - public CommandLineBuilder EnableDirectives(bool value = true) - { - EnableDirectivesFlag = value; - - return this; - } - /// /// Enables the parser to recognize and expand POSIX-style bundled options. /// @@ -127,13 +113,11 @@ await feature.EnsureRegistered(async () => return this; } - /// - /// Enables the use of the [env:key=value] directive, allowing environment variables to be set from the command line during invocation. - /// + /// /// The reference to this instance. public CommandLineBuilder UseEnvironmentVariableDirective() { - EnableEnvironmentVariableDirective = true; + Directives.Add(new EnvironmentVariablesDirective()); return this; } @@ -307,15 +291,14 @@ public CommandLineBuilder AddMiddleware( }, order); } - /// - /// Enables the use of the [parse] directive, which when specified on the command line will short circuit normal command handling and display a diagram explaining the parse result for the command line input. - /// + + /// /// If the parse result contains errors, this exit code will be used when the process exits. /// The reference to this instance. public CommandLineBuilder UseParseDirective( int errorExitCode = 1) { - ParseDirectiveExitCode = errorExitCode; + Directives.Add(new ParseDirective(errorExitCode)); return this; } @@ -333,14 +316,11 @@ public CommandLineBuilder UseParseErrorReporting( return this; } - /// - /// Enables the use of the [suggest] directive which when specified in command line input short circuits normal command handling and writes a newline-delimited list of suggestions suitable for use by most shells to provide command line completions. - /// - /// The dotnet-suggest tool requires the suggest directive to be enabled for an application to provide completions. + /// /// The reference to this instance. public CommandLineBuilder UseSuggestDirective() { - EnableSuggestDirective = true; + Directives.Add(new SuggestDirective()); return this; } diff --git a/src/System.CommandLine/CommandLineConfiguration.cs b/src/System.CommandLine/CommandLineConfiguration.cs index 6b5fcb9173..efd791aa35 100644 --- a/src/System.CommandLine/CommandLineConfiguration.cs +++ b/src/System.CommandLine/CommandLineConfiguration.cs @@ -23,22 +23,6 @@ public class CommandLineConfiguration /// internal readonly Action? ExceptionHandler; - /// - /// Enables the use of the [env:key=value] directive, allowing environment variables to be set from the command line during invocation. - /// - internal readonly bool EnableEnvironmentVariableDirective; - - /// - /// If the parse result contains errors, this exit code will be used when the process exits. - /// - internal readonly int? ParseDirectiveExitCode; - - /// - /// Enables the use of the [suggest] directive which when specified in command line input short circuits normal command handling and writes a newline-delimited list of suggestions suitable for use by most shells to provide command line completions. - /// - /// The dotnet-suggest tool requires the suggest directive to be enabled for an application to provide completions. - internal readonly bool EnableSuggestDirective; - /// /// The exit code to use when parser errors occur. /// @@ -61,7 +45,6 @@ public class CommandLineConfiguration /// /// The root command for the parser. /// to enable POSIX bundling; otherwise, . - /// to enable directive parsing; otherwise, . /// to enable token replacement; otherwise, . /// Provide a custom middleware pipeline. /// Provide a custom help builder. @@ -69,24 +52,30 @@ public class CommandLineConfiguration public CommandLineConfiguration( Command command, bool enablePosixBundling = true, - bool enableDirectives = true, bool enableTokenReplacement = true, IReadOnlyList? middlewarePipeline = null, Func? helpBuilderFactory = null, TryReplaceToken? tokenReplacer = null) - : this(command, enablePosixBundling, enableDirectives, enableTokenReplacement, false, null, false, null, 0, null, - middlewarePipeline, helpBuilderFactory, tokenReplacer, null) + : this( + command, + directives: null, + enablePosixBundling: enablePosixBundling, + enableTokenReplacement: enableTokenReplacement, + parseErrorReportingExitCode: null, + maxLevenshteinDistance: 0, + processTerminationTimeout: null, + middlewarePipeline: middlewarePipeline, + helpBuilderFactory: helpBuilderFactory, + tokenReplacer: tokenReplacer, + exceptionHandler: null) { } internal CommandLineConfiguration( Command command, + List? directives, bool enablePosixBundling, - bool enableDirectives, bool enableTokenReplacement, - bool enableEnvironmentVariableDirective, - int? parseDirectiveExitCode, - bool enableSuggestDirective, int? parseErrorReportingExitCode, int maxLevenshteinDistance, TimeSpan? processTerminationTimeout, @@ -96,12 +85,9 @@ internal CommandLineConfiguration( Action? exceptionHandler) { RootCommand = command ?? throw new ArgumentNullException(nameof(command)); + Directives = directives is not null ? directives : Array.Empty(); EnableTokenReplacement = enableTokenReplacement; EnablePosixBundling = enablePosixBundling; - EnableDirectives = enableDirectives || enableEnvironmentVariableDirective || parseDirectiveExitCode.HasValue || enableSuggestDirective; - EnableEnvironmentVariableDirective = enableEnvironmentVariableDirective; - ParseDirectiveExitCode = parseDirectiveExitCode; - EnableSuggestDirective = enableSuggestDirective; ParseErrorReportingExitCode = parseErrorReportingExitCode; MaxLevenshteinDistance = maxLevenshteinDistance; ProcessTerminationTimeout = processTerminationTimeout; @@ -126,9 +112,9 @@ internal static HelpBuilder DefaultHelpBuilderFactory(BindingContext context, in } /// - /// Gets whether directives are enabled. + /// Gets the enabled directives. /// - public bool EnableDirectives { get; } + public IReadOnlyList Directives { get; } /// /// Gets a value indicating whether POSIX bundling is enabled. diff --git a/src/System.CommandLine/Directive.cs b/src/System.CommandLine/Directive.cs new file mode 100644 index 0000000000..e299058ad8 --- /dev/null +++ b/src/System.CommandLine/Directive.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.CommandLine.Completions; +using System.CommandLine.Invocation; +using System.Threading.Tasks; +using System.Threading; + +namespace System.CommandLine +{ + /// + /// The purpose of directives is to provide cross-cutting functionality that can apply across command-line apps. + /// Because directives are syntactically distinct from the app's own syntax, they can provide functionality that applies across apps. + /// + /// A directive must conform to the following syntax rules: + /// * It's a token on the command line that comes after the app's name but before any subcommands or options. + /// * It's enclosed in square brackets. + /// * It doesn't contain spaces. + /// + public class Directive : Symbol + { + /// + /// Initializes a new instance of the Directive class. + /// + /// The name of the directive. It can't contain whitespaces. + /// The synchronous action that is invoked when directive is parsed. + /// The asynchronous action that is invoked when directive is parsed. + public Directive(string name, + Action? syncHandler = null, + Func? asyncHandler = null) + : base(name) + { + if (syncHandler is not null) + { + SetSynchronousHandler(syncHandler); + } + else if (asyncHandler is not null) + { + SetAsynchronousHandler(asyncHandler); + } + } + + public void SetAsynchronousHandler(Func handler) + { + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } + + Handler = new AnonymousCommandHandler(handler); + } + + public void SetSynchronousHandler(Action handler) + { + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } + + Handler = new AnonymousCommandHandler(handler); + } + + internal ICommandHandler? Handler { get; private set; } + + public override IEnumerable GetCompletions(CompletionContext context) + => Array.Empty(); + } +} diff --git a/src/System.CommandLine/DirectiveCollection.cs b/src/System.CommandLine/DirectiveCollection.cs deleted file mode 100644 index 45a469d360..0000000000 --- a/src/System.CommandLine/DirectiveCollection.cs +++ /dev/null @@ -1,85 +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.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -namespace System.CommandLine -{ - /// - /// A collection of directives parsed from a command line. - /// - /// A directive is specified on the command line using square brackets, containing no spaces and preceding other tokens unless they are also directives. In the following example, two directives are present, directive-one and directive-two: - /// > myapp [directive-one] [directive-two:value] arg1 arg2 - /// The second has a value specified as well, value. Directive values can be read by calling using . - /// - public class DirectiveCollection : IEnumerable>> - { - private Dictionary>? _directives; - - internal void Add(string name, string? value) - { - _directives ??= new(); - - if (!_directives.TryGetValue(name, out var values)) - { - values = new List(); - - _directives.Add(name, values); - } - - if (value is not null) - { - values.Add(value); - } - } - - /// - /// Gets a value determining whether a directive with the specified name was parsed. - /// - /// The name of the directive. - /// if a directive with the specified name was parsed; otherwise, . - public bool Contains(string name) - { - return _directives is not null && _directives.ContainsKey(name); - } - - /// - /// Gets the values specified for a given directive. A return value indicates whether the specified directive name was present. - /// - /// The name of the directive. - /// The values provided for the specified directive. - /// if a directive with the specified name was parsed; otherwise, . - public bool TryGetValues(string name, [NotNullWhen(true)] out IReadOnlyList? values) - { - if (_directives is not null && - _directives.TryGetValue(name, out var v)) - { - values = v; - return true; - } - else - { - values = null; - return false; - } - } - - /// - public IEnumerator>> GetEnumerator() - { - if (_directives is null) - { - return Enumerable.Empty>>().GetEnumerator(); - } - - return _directives - .Select(pair => new KeyValuePair>(pair.Key, pair.Value)) - .GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/src/System.CommandLine/EnvironmentVariablesDirective.cs b/src/System.CommandLine/EnvironmentVariablesDirective.cs new file mode 100644 index 0000000000..6f65d52c87 --- /dev/null +++ b/src/System.CommandLine/EnvironmentVariablesDirective.cs @@ -0,0 +1,43 @@ +using System.CommandLine.Invocation; +using System.CommandLine.Parsing; + +namespace System.CommandLine +{ + /// + /// Enables the use of the [env:key=value] directive, allowing environment variables to be set from the command line during invocation. + /// + public sealed class EnvironmentVariablesDirective : Directive + { + public EnvironmentVariablesDirective() : base("env") + { + SetSynchronousHandler(SyncHandler); + } + + private void SyncHandler(InvocationContext context) + { + DirectiveResult directiveResult = context.ParseResult.FindResultFor(this)!; + + for (int i = 0; i < directiveResult.Values.Count; i++) + { + string parsedValue = directiveResult.Values[i]; + + int indexOfSeparator = parsedValue.AsSpan().IndexOf('='); + + if (indexOfSeparator > 0) + { + ReadOnlySpan variable = parsedValue.AsSpan(0, indexOfSeparator).Trim(); + + if (!variable.IsEmpty) + { + string value = parsedValue.AsSpan(indexOfSeparator + 1).Trim().ToString(); + + Environment.SetEnvironmentVariable(variable.ToString(), value); + } + } + } + + // we need a cleaner, more flexible and intuitive way of continuing the execution + context.ParseResult.CommandResult.Command.Handler?.Invoke(context); + } + } +} diff --git a/src/System.CommandLine/Invocation/ParseDirectiveResult.cs b/src/System.CommandLine/Invocation/ParseDirectiveResult.cs deleted file mode 100644 index 005c1844c7..0000000000 --- a/src/System.CommandLine/Invocation/ParseDirectiveResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.CommandLine.IO; -using System.CommandLine.Parsing; - -namespace System.CommandLine.Invocation -{ - internal static class ParseDirectiveResult - { - internal static void Apply(InvocationContext context) - { - var parseResult = context.ParseResult; - context.Console.Out.WriteLine(parseResult.Diagram()); - context.ExitCode = parseResult.Errors.Count == 0 - ? 0 - : context.ParseResult.Configuration.ParseDirectiveExitCode!.Value; - } - } -} diff --git a/src/System.CommandLine/Invocation/SuggestDirectiveResult.cs b/src/System.CommandLine/Invocation/SuggestDirectiveResult.cs deleted file mode 100644 index aeb404c51b..0000000000 --- a/src/System.CommandLine/Invocation/SuggestDirectiveResult.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. - -using System.CommandLine.IO; -using System.CommandLine.Parsing; -using System.Linq; - -namespace System.CommandLine.Invocation -{ - internal static class SuggestDirectiveResult - { - internal static void Apply(InvocationContext context, int position) - { - var commandLineToComplete = context.ParseResult.Tokens.LastOrDefault(t => t.Type != TokenType.Directive)?.Value ?? ""; - - var completionParseResult = context.ParseResult.RootCommandResult.Command.Parse(commandLineToComplete, context.ParseResult.Configuration); - - var completions = completionParseResult.GetCompletions(position); - - context.Console.Out.WriteLine( - string.Join( - Environment.NewLine, - completions)); - } - } -} diff --git a/src/System.CommandLine/ParseDirective.cs b/src/System.CommandLine/ParseDirective.cs new file mode 100644 index 0000000000..e6235caae4 --- /dev/null +++ b/src/System.CommandLine/ParseDirective.cs @@ -0,0 +1,28 @@ +using System.CommandLine.Invocation; +using System.CommandLine.IO; +using System.CommandLine.Parsing; + +namespace System.CommandLine +{ + /// + /// Enables the use of the [parse] directive, which when specified on the command line will short circuit normal command handling and display a diagram explaining the parse result for the command line input. + /// + public sealed class ParseDirective : Directive + { + /// If the parse result contains errors, this exit code will be used when the process exits. + public ParseDirective(int errorExitCode = 1) : base("parse") + { + SetSynchronousHandler(SyncHandler); + ErrorExitCode = errorExitCode; + } + + internal int ErrorExitCode { get; } + + private void SyncHandler(InvocationContext context) + { + var parseResult = context.ParseResult; + context.Console.Out.WriteLine(parseResult.Diagram()); + context.ExitCode = parseResult.Errors.Count == 0 ? 0 : ErrorExitCode; + } + } +} diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index c8144c127b..76872fc967 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -20,7 +20,6 @@ public class ParseResult private readonly IReadOnlyList _errors; private readonly CommandResult _rootCommandResult; private readonly IReadOnlyList _unmatchedTokens; - private Dictionary>? _directives; private CompletionContext? _completionContext; private ICommandHandler? _handler; @@ -28,7 +27,6 @@ internal ParseResult( CommandLineConfiguration configuration, CommandResult rootCommandResult, CommandResult commandResult, - Dictionary>? directives, List tokens, IReadOnlyList? unmatchedTokens, List? errors, @@ -38,7 +36,6 @@ internal ParseResult( Configuration = configuration; _rootCommandResult = rootCommandResult; CommandResult = commandResult; - _directives = directives; _handler = handler; // skip the root command when populating Tokens property @@ -82,12 +79,6 @@ internal ParseResult( /// public IReadOnlyList Errors => _errors; - /// - /// Gets the directives found while parsing command line input. - /// - /// If is set to , then this collection will be empty. - public IReadOnlyDictionary> Directives => _directives ??= new (); - /// /// Gets the tokens identified while parsing command line input. /// @@ -165,19 +156,20 @@ CommandLineText is null public OptionResult? FindResultFor(Option option) => _rootCommandResult.FindResultFor(option); + /// + /// 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? FindResultFor(Directive directive) => _rootCommandResult.FindResultFor(directive); + /// /// Gets the result, if any, for the specified symbol. /// /// 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? FindResultFor(Symbol symbol) => - symbol switch - { - Argument argument => FindResultFor(argument), - Command command => FindResultFor(command), - Option option => FindResultFor(option), - _ => throw new ArgumentOutOfRangeException(nameof(symbol)) - }; + public SymbolResult? FindResultFor(Symbol symbol) + => _rootCommandResult.SymbolResultTree.TryGetValue(symbol, out SymbolResult? result) ? result : null; /// /// Gets completions based on a given parse result. @@ -193,6 +185,7 @@ public IEnumerable GetCompletions( { ArgumentResult argumentResult => argumentResult.Argument, OptionResult optionResult => optionResult.Option, + DirectiveResult directiveResult => directiveResult.Directive, _ => ((CommandResult)currentSymbolResult).Command }; diff --git a/src/System.CommandLine/Parsing/DirectiveResult.cs b/src/System.CommandLine/Parsing/DirectiveResult.cs new file mode 100644 index 0000000000..78ceeaf0d7 --- /dev/null +++ b/src/System.CommandLine/Parsing/DirectiveResult.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace System.CommandLine.Parsing +{ + /// + /// A result produced when parsing an . + /// + public sealed class DirectiveResult : SymbolResult + { + private List? _values; + + internal DirectiveResult(Directive directive, Token token, SymbolResultTree symbolResultTree) + : base(symbolResultTree, null) // directives don't belong to any command + { + Directive = directive; + Token = token; + } + + /// + /// Parsed values of [name:value] directive(s). + /// + /// Can be empty for [name] directives. + public IReadOnlyList Values => _values is null ? Array.Empty() : _values; + + /// + /// The directive to which the result applies. + /// + public Directive Directive { get; } + + /// + /// The token that was parsed to specify the directive. + /// + public Token Token { get; } + + internal void AddValue(string value) => (_values ??= new()).Add(value); + } +} diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 8741adc7b6..63a7e020b4 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -16,7 +16,6 @@ internal sealed class ParseOperation private readonly CommandResult _rootCommandResult; private int _index; - private Dictionary>? _directives; private CommandResult _innermostCommandResult; private bool _isHelpRequested, _isParseRequested; private ICommandHandler? _handler; @@ -78,7 +77,6 @@ internal ParseResult Parse() _configuration, _rootCommandResult, _innermostCommandResult, - _directives, _tokens, _symbolResultTree.UnmatchedTokens, _symbolResultTree.Errors, @@ -285,7 +283,7 @@ private void ParseDirectives() { while (More(out TokenType currentTokenType) && currentTokenType == TokenType.Directive) { - if (_configuration.EnableDirectives) + if (_configuration.Directives.Count > 0) { ParseDirective(); // kept in separate method to avoid JIT } @@ -296,28 +294,38 @@ private void ParseDirectives() void ParseDirective() { var token = CurrentToken; - ReadOnlySpan withoutBrackets = token.Value.AsSpan(1, token.Value.Length - 2); - int indexOfColon = withoutBrackets.IndexOf(':'); - string key = indexOfColon >= 0 - ? withoutBrackets.Slice(0, indexOfColon).ToString() - : withoutBrackets.ToString(); - string? value = indexOfColon > 0 - ? withoutBrackets.Slice(indexOfColon + 1).ToString() - : null; - - if (_directives is null || !_directives.TryGetValue(key, out var values)) + + if (token.Symbol is not Directive directive) { - values = new List(); + AddCurrentTokenToUnmatched(); + return; + } - (_directives ??= new()).Add(key, values); + DirectiveResult result; + if (_symbolResultTree.TryGetValue(directive, out var directiveResult)) + { + result = (DirectiveResult)directiveResult; + result.AddToken(token); + } + else + { + result = new (directive, token, _symbolResultTree); + _symbolResultTree.Add(directive, result); } - if (value is not null) + ReadOnlySpan withoutBrackets = token.Value.AsSpan(1, token.Value.Length - 2); + int indexOfColon = withoutBrackets.IndexOf(':'); + if (indexOfColon > 0) { - ((List)values).Add(value); + result.AddValue(withoutBrackets.Slice(indexOfColon + 1).ToString()); } - OnDirectiveParsed(key, value); + _handler = directive.Handler; + + if (directive is ParseDirective) + { + _isParseRequested = true; + } } } @@ -346,35 +354,5 @@ private void Validate() currentResult = currentResult.Parent as CommandResult; } } - - private void OnDirectiveParsed(string directiveKey, string? parsedValues) - { - if (_configuration.EnableEnvironmentVariableDirective && directiveKey == "env") - { - if (parsedValues is not null) - { - var components = parsedValues.Split(new[] { '=' }, count: 2); - var variable = components.Length > 0 ? components[0].Trim() : string.Empty; - if (string.IsNullOrEmpty(variable) || components.Length < 2) - { - return; - } - - var value = components[1].Trim(); - Environment.SetEnvironmentVariable(variable, value); - } - } - else if (_configuration.ParseDirectiveExitCode.HasValue && directiveKey == "parse") - { - _isParseRequested = true; - _handler = new AnonymousCommandHandler(ParseDirectiveResult.Apply); - } - else if (_configuration.EnableSuggestDirective && directiveKey == "suggest") - { - int position = parsedValues is not null ? int.Parse(parsedValues) : _rawInput?.Length ?? 0; - - _handler = new AnonymousCommandHandler(ctx => SuggestDirectiveResult.Apply(ctx, position)); - } - } } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/ParseResultExtensions.cs b/src/System.CommandLine/Parsing/ParseResultExtensions.cs index d72cb01853..0bea7b7afc 100644 --- a/src/System.CommandLine/Parsing/ParseResultExtensions.cs +++ b/src/System.CommandLine/Parsing/ParseResultExtensions.cs @@ -59,6 +59,8 @@ private static void Diagram( switch (symbolResult) { + case DirectiveResult directiveResult when directiveResult.Directive is not ParseDirective: + break; case ArgumentResult argumentResult: { var includeArgumentName = diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index e08a01ace3..4c5e649b32 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -57,11 +57,11 @@ internal static void Tokenize( var currentCommand = configuration.RootCommand; var foundDoubleDash = false; - var foundEndOfDirectives = !configuration.EnableDirectives; + var foundEndOfDirectives = false; var tokenList = new List(args.Count); - var knownTokens = configuration.RootCommand.ValidTokens(); + var knownTokens = configuration.RootCommand.ValidTokens(configuration.Directives); int i = FirstArgumentIsRootCommand(args, configuration.RootCommand, inferRootCommand) ? 0 @@ -96,7 +96,16 @@ internal static void Tokenize( arg[1] != ':' && arg[arg.Length - 1] == ']') { - tokenList.Add(Directive(arg)); + 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 + + Directive? directive = knownTokens.TryGetValue(directiveName, out var directiveToken) + ? (Directive)directiveToken.Symbol! + : null; + + tokenList.Add(Directive(arg, directive)); continue; } @@ -150,7 +159,8 @@ configuration.TokenReplacer is { } replacer && { if (cmd != configuration.RootCommand) { - knownTokens = cmd.ValidTokens(); + knownTokens = cmd.ValidTokens( + directives: null); // config contains Directives, they are allowed only for RootCommand } currentCommand = cmd; tokenList.Add(Command(arg, cmd)); @@ -194,7 +204,7 @@ configuration.TokenReplacer is { } replacer && Token DoubleDash() => new("--", TokenType.DoubleDash, default, i); - Token Directive(string value) => new(value, TokenType.Directive, default, i); + Token Directive(string value, Directive? directive) => new(value, TokenType.Directive, directive, i); } tokens = tokenList; @@ -396,10 +406,21 @@ static IEnumerable SplitLine(string line) } } - private static Dictionary ValidTokens(this Command command) + private static Dictionary ValidTokens(this Command command, IReadOnlyList? directives) { Dictionary tokens = new(StringComparer.Ordinal); + if (directives is not null) + { + for (int directiveIndex = 0; directiveIndex < directives.Count; directiveIndex++) + { + Directive directive = directives[directiveIndex]; + tokens.Add( + directive.Name, + new Token(directive.Name, TokenType.Directive, directive, Token.ImplicitPosition)); + } + } + AddCommandTokens(tokens, command); if (command.HasSubcommands) diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/SymbolResult.cs index 438b038e17..fcebe95aae 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/SymbolResult.cs @@ -60,6 +60,13 @@ private protected SymbolResult(SymbolResultTree symbolResultTree, SymbolResult? /// An option result if the option was matched by the parser or has a default value; otherwise, null. public OptionResult? FindResultFor(Option option) => SymbolResultTree.FindResultFor(option); + /// + /// 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? FindResultFor(Directive directive) => SymbolResultTree.FindResultFor(directive); + /// public T? GetValue(Argument argument) { diff --git a/src/System.CommandLine/Parsing/SymbolResultExtensions.cs b/src/System.CommandLine/Parsing/SymbolResultExtensions.cs index 195fc04cb6..c81459e8c6 100644 --- a/src/System.CommandLine/Parsing/SymbolResultExtensions.cs +++ b/src/System.CommandLine/Parsing/SymbolResultExtensions.cs @@ -18,5 +18,6 @@ internal static IEnumerable AllSymbolResults(this CommandResult co yield return item; } } + } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 86bcdbe66d..b8f3245233 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -34,6 +34,9 @@ internal SymbolResultTree(List? tokenizeErrors) internal OptionResult? FindResultFor(Option option) => TryGetValue(option, out SymbolResult? result) ? (OptionResult)result : default; + internal DirectiveResult? FindResultFor(Directive directive) + => TryGetValue(directive, out SymbolResult? result) ? (DirectiveResult)result : default; + internal IEnumerable GetChildren(SymbolResult parent) { if (parent is not ArgumentResult) diff --git a/src/System.CommandLine/Parsing/TokenType.cs b/src/System.CommandLine/Parsing/TokenType.cs index 679ba9b799..0aae333afa 100644 --- a/src/System.CommandLine/Parsing/TokenType.cs +++ b/src/System.CommandLine/Parsing/TokenType.cs @@ -30,11 +30,11 @@ public enum TokenType /// A double dash (--) token, which changes the meaning of subsequent tokens. /// DoubleDash, - + /// /// A directive token. /// - /// + /// Directive } } diff --git a/src/System.CommandLine/SuggestDirective.cs b/src/System.CommandLine/SuggestDirective.cs new file mode 100644 index 0000000000..c0c445eefb --- /dev/null +++ b/src/System.CommandLine/SuggestDirective.cs @@ -0,0 +1,39 @@ +using System.CommandLine.Invocation; +using System.Linq; +using System.CommandLine.IO; +using System.CommandLine.Parsing; + +namespace System.CommandLine +{ + /// + /// Enables the use of the [suggest] directive which when specified in command line input short circuits normal command handling and writes a newline-delimited list of suggestions suitable for use by most shells to provide command line completions. + /// + /// The dotnet-suggest tool requires the suggest directive to be enabled for an application to provide completions. + public sealed class SuggestDirective : Directive + { + public SuggestDirective() : base("suggest") + { + SetSynchronousHandler(SyncHandler); + } + + private void SyncHandler(InvocationContext context) + { + ParseResult parseResult = context.ParseResult; + string? parsedValues = parseResult.FindResultFor(this)!.Values.SingleOrDefault(); + string? rawInput = parseResult.CommandLineText; + + int position = !string.IsNullOrEmpty(parsedValues) ? int.Parse(parsedValues) : rawInput?.Length ?? 0; + + var commandLineToComplete = parseResult.Tokens.LastOrDefault(t => t.Type != TokenType.Directive)?.Value ?? ""; + + var completionParseResult = parseResult.RootCommandResult.Command.Parse(commandLineToComplete, parseResult.Configuration); + + var completions = completionParseResult.GetCompletions(position); + + context.Console.Out.WriteLine( + string.Join( + Environment.NewLine, + completions)); + } + } +}