From 6f184070f07193fa20dfb65df8f0839bcb1177e6 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Tue, 15 May 2018 14:35:09 -0700 Subject: [PATCH 1/3] Improve command completion. This commit improves command completion by updating the `new` and `nuget` parsers to match their current supported syntax. Removes the unnecessary description strings that were not used (these commands are parsed by assemblies external to the CLI). The top level options are also sync'd to the currently supported options. Additionally, it unhides the `msbuild` and `vstest` commands so that `dotnet complete` suggests them. Fixes #9286. --- src/dotnet/Parser.cs | 23 +++- .../commands/dotnet-new/NewCommandParser.cs | 39 ------ .../commands/dotnet-new/NewCommandShim.cs | 2 +- .../dotnet-nuget/NuGetCommandParser.cs | 112 +++++++----------- test/dotnet.Tests/TelemetryCommandTest.cs | 2 +- 5 files changed, 64 insertions(+), 114 deletions(-) delete mode 100644 src/dotnet/commands/dotnet-new/NewCommandParser.cs diff --git a/src/dotnet/Parser.cs b/src/dotnet/Parser.cs index 644ddd5c0..0697058d4 100644 --- a/src/dotnet/Parser.cs +++ b/src/dotnet/Parser.cs @@ -3,14 +3,23 @@ using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Tools.Help; +using Microsoft.DotNet.Tools.New; using static System.Environment; using static Microsoft.DotNet.Cli.CommandLine.LocalizableStrings; using LocalizableStrings = Microsoft.DotNet.Tools.Run.LocalizableStrings; +using NewCommandParser = Microsoft.TemplateEngine.Cli.CommandParsing.CommandParserSupport; namespace Microsoft.DotNet.Cli { public static class Parser { + // This is used for descriptions of commands and options that are only defined for `dotnet complete` (i.e. command line completion). + // For example, a NuGet assembly handles parsing the `nuget` command and options. + // To get completion for such a command, we have to define a parser that is used for the completion. + // Command and option help text cannot be empty, otherwise the parser will hide them from the completion list. + // The value of `-` has no special meaning; it simply prevents these commands and options from being hidden. + internal const string CompletionOnlyDescription = "-"; + static Parser() { ConfigureCommandLineLocalizedStrings(); @@ -35,7 +44,7 @@ namespace Microsoft.DotNet.Cli options: Create.Command("dotnet", ".NET Command Line Tools", Accept.NoArguments(), - NewCommandParser.New(), + NewCommandParser.CreateNewCommandWithoutTemplateInfo(NewCommandShim.CommandName), RestoreCommandParser.Restore(), BuildCommandParser.Build(), PublishCommandParser.Publish(), @@ -51,15 +60,17 @@ namespace Microsoft.DotNet.Cli NuGetCommandParser.NuGet(), StoreCommandParser.Store(), HelpCommandParser.Help(), - Create.Command("msbuild", ""), - Create.Command("vstest", ""), + Create.Command("msbuild", CompletionOnlyDescription), + Create.Command("vstest", CompletionOnlyDescription), CompleteCommandParser.Complete(), InternalReportinstallsuccessCommandParser.InternalReportinstallsuccess(), ToolCommandParser.Tool(), BuildServerCommandParser.CreateCommand(), CommonOptions.HelpOption(), - Create.Option("--info", ""), - Create.Option("-d", ""), - Create.Option("--debug", ""))); + Create.Option("--info", CompletionOnlyDescription), + Create.Option("-d|--diagnostics", CompletionOnlyDescription), + Create.Option("--version", CompletionOnlyDescription), + Create.Option("--list-sdks", CompletionOnlyDescription), + Create.Option("--list-runtimes", CompletionOnlyDescription))); } } diff --git a/src/dotnet/commands/dotnet-new/NewCommandParser.cs b/src/dotnet/commands/dotnet-new/NewCommandParser.cs deleted file mode 100644 index 88d81208a..000000000 --- a/src/dotnet/commands/dotnet-new/NewCommandParser.cs +++ /dev/null @@ -1,39 +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 Microsoft.DotNet.Cli.CommandLine; - -namespace Microsoft.DotNet.Cli -{ - internal static class NewCommandParser - { - public static Command New() => - Create.Command("new", - "Initialize .NET projects.", - Accept - .ExactlyOneArgument() - .WithSuggestionsFrom( - "console", - "classlib", - "mstest", - "xunit", - "web", - "mvc", - "webapi", - "sln"), - Create.Option("-l|--list", - "List templates containing the specified name."), - Create.Option("-lang|--language", - "Specifies the language of the template to create", - Accept.WithSuggestionsFrom("C#", "F#") - .With(defaultValue: () => "C#")), - Create.Option("-n|--name", - "The name for the output being created. If no name is specified, the name of the current directory is used."), - Create.Option("-o|--output", - "Location to place the generated output."), - Create.Option("-h|--help", - "Displays help for this command."), - Create.Option("-all|--show-all", - "Shows all templates")); - } -} \ No newline at end of file diff --git a/src/dotnet/commands/dotnet-new/NewCommandShim.cs b/src/dotnet/commands/dotnet-new/NewCommandShim.cs index 8c0c2acd4..b01f3c143 100644 --- a/src/dotnet/commands/dotnet-new/NewCommandShim.cs +++ b/src/dotnet/commands/dotnet-new/NewCommandShim.cs @@ -24,8 +24,8 @@ namespace Microsoft.DotNet.Tools.New { internal class NewCommandShim { + public const string CommandName = "new"; private const string HostIdentifier = "dotnetcli"; - private const string CommandName = "new"; public static int Run(string[] args) { diff --git a/src/dotnet/commands/dotnet-nuget/NuGetCommandParser.cs b/src/dotnet/commands/dotnet-nuget/NuGetCommandParser.cs index fd80e5589..8832950c2 100644 --- a/src/dotnet/commands/dotnet-nuget/NuGetCommandParser.cs +++ b/src/dotnet/commands/dotnet-nuget/NuGetCommandParser.cs @@ -5,74 +5,52 @@ using Microsoft.DotNet.Cli.CommandLine; namespace Microsoft.DotNet.Cli { + // This parser is used for completion and telemetry. + // See https://github.com/NuGet/NuGet.Client for the actual implementation. internal static class NuGetCommandParser { public static Command NuGet() => - Create.Command("nuget", - "NuGet Command Line 4.0.0.0", - CommonOptions.HelpOption(), - Create.Option("--version", - "Show version information"), - Create.Option("-v|--verbosity", - "The verbosity of logging to use. Allowed values: Debug, Verbose, Information, Minimal, Warning, Error.", - Accept.ExactlyOneArgument() - .With(name: "verbosity")), - Create.Command("delete", - "Deletes a package from the server.", - Accept.ExactlyOneArgument() - .With(name: "root", - description: "The Package Id and version."), - CommonOptions.HelpOption(), - Create.Option("--force-english-output", - "Forces the application to run using an invariant, English-based culture."), - Create.Option("-s|--source", - "Specifies the server URL", - Accept.ExactlyOneArgument() - .With(name: "source")), - Create.Option("--non-interactive", - "Do not prompt for user input or confirmations."), - Create.Option("-k|--api-key", - "The API key for the server.", - Accept.ExactlyOneArgument() - .With(name: "apiKey"))), - Create.Command("locals", - "Clears or lists local NuGet resources such as http requests cache, packages cache or machine-wide global packages folder.", - Accept.AnyOneOf(@"all", - @"http-cache", - @"global-packages", - @"temp") - .With(description: "Cache Location(s) Specifies the cache location(s) to list or clear."), - CommonOptions.HelpOption(), - Create.Option("--force-english-output", - "Forces the application to run using an invariant, English-based culture."), - Create.Option("-c|--clear", "Clear the selected local resources or cache location(s)."), - Create.Option("-l|--list", "List the selected local resources or cache location(s).")), - Create.Command("push", - "Pushes a package to the server and publishes it.", - CommonOptions.HelpOption(), - Create.Option("--force-english-output", - "Forces the application to run using an invariant, English-based culture."), - Create.Option("-s|--source", - "Specifies the server URL", - Accept.ExactlyOneArgument() - .With(name: "source")), - Create.Option("-ss|--symbol-source", - "Specifies the symbol server URL. If not specified, nuget.smbsrc.net is used when pushing to nuget.org.", - Accept.ExactlyOneArgument() - .With(name: "source")), - Create.Option("-t|--timeout", - "Specifies the timeout for pushing to a server in seconds. Defaults to 300 seconds (5 minutes).", - Accept.ExactlyOneArgument() - .With(name: "timeout")), - Create.Option("-k|--api-key", "The API key for the server.", - Accept.ExactlyOneArgument() - .With(name: "apiKey")), - Create.Option("-sk|--symbol-api-key", "The API key for the symbol server.", - Accept.ExactlyOneArgument() - .With(name: "apiKey")), - Create.Option("-d|--disable-buffering", - "Disable buffering when pushing to an HTTP(S) server to decrease memory usage."), - Create.Option("-n|--no-symbols", - "If a symbols package exists, it will not be pushed to a symbols server."))); + Create.Command( + "nuget", + Parser.CompletionOnlyDescription, + Create.Option("-h|--help", Parser.CompletionOnlyDescription), + Create.Option("--version", Parser.CompletionOnlyDescription), + Create.Option("-v|--verbosity", Parser.CompletionOnlyDescription, Accept.ExactlyOneArgument()), + Create.Command( + "delete", + Parser.CompletionOnlyDescription, + Accept.OneOrMoreArguments(), + Create.Option("-h|--help", Parser.CompletionOnlyDescription), + Create.Option("--force-english-output", Parser.CompletionOnlyDescription), + Create.Option("-s|--source", Parser.CompletionOnlyDescription, Accept.ExactlyOneArgument()), + Create.Option("--non-interactive", Parser.CompletionOnlyDescription), + Create.Option("-k|--api-key", Parser.CompletionOnlyDescription, Accept.ExactlyOneArgument()), + Create.Option("--no-service-endpoint", Parser.CompletionOnlyDescription)), + Create.Command( + "locals", + Parser.CompletionOnlyDescription, + Accept.AnyOneOf( + "all", + "http-cache", + "global-packages", + "temp"), + Create.Option("-h|--help", Parser.CompletionOnlyDescription), + Create.Option("--force-english-output", Parser.CompletionOnlyDescription), + Create.Option("-c|--clear", Parser.CompletionOnlyDescription), + Create.Option("-l|--list", Parser.CompletionOnlyDescription)), + Create.Command( + "push", + Parser.CompletionOnlyDescription, + Accept.OneOrMoreArguments(), + Create.Option("-h|--help", Parser.CompletionOnlyDescription), + Create.Option("--force-english-output", Parser.CompletionOnlyDescription), + Create.Option("-s|--source", Parser.CompletionOnlyDescription, Accept.ExactlyOneArgument()), + Create.Option("-ss|--symbol-source", Parser.CompletionOnlyDescription, Accept.ExactlyOneArgument()), + Create.Option("-t|--timeout", Parser.CompletionOnlyDescription, Accept.ExactlyOneArgument()), + Create.Option("-k|--api-key", Parser.CompletionOnlyDescription, Accept.ExactlyOneArgument()), + Create.Option("-sk|--symbol-api-key", Parser.CompletionOnlyDescription, Accept.ExactlyOneArgument()), + Create.Option("-d|--disable-buffering", Parser.CompletionOnlyDescription), + Create.Option("-n|--no-symbols", Parser.CompletionOnlyDescription), + Create.Option("--no-service-endpoint", Parser.CompletionOnlyDescription))); } -} \ No newline at end of file +} diff --git a/test/dotnet.Tests/TelemetryCommandTest.cs b/test/dotnet.Tests/TelemetryCommandTest.cs index 3668dee1a..3ab0b0f18 100644 --- a/test/dotnet.Tests/TelemetryCommandTest.cs +++ b/test/dotnet.Tests/TelemetryCommandTest.cs @@ -163,7 +163,7 @@ namespace Microsoft.DotNet.Tests { const string argumentToSend = "push"; - string[] args = { "nuget", argumentToSend }; + string[] args = { "nuget", argumentToSend, "path" }; Cli.Program.ProcessArgs(args); _fakeTelemetry From 089d8bfc1b8cf8a7f1f3f790b8288cfabfb3c3b4 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Tue, 15 May 2018 23:36:55 -0700 Subject: [PATCH 2/3] Remove `internal-reportinstallsuccess` from `dotnet complete`. This commit removes `internal-reportinstallsuccess` from `dotnet complete` by changing the command's help text to an empty string. This causes the parser to treat the command as hidden and does not match the command name for suggestions. Fixes #9111. --- .../InternalReportinstallsuccessCommandParser.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dotnet/commands/dotnet-internal-reportinstallsuccess/InternalReportinstallsuccessCommandParser.cs b/src/dotnet/commands/dotnet-internal-reportinstallsuccess/InternalReportinstallsuccessCommandParser.cs index 1c83a86b3..303abb1fe 100644 --- a/src/dotnet/commands/dotnet-internal-reportinstallsuccess/InternalReportinstallsuccessCommandParser.cs +++ b/src/dotnet/commands/dotnet-internal-reportinstallsuccess/InternalReportinstallsuccessCommandParser.cs @@ -10,7 +10,8 @@ namespace Microsoft.DotNet.Cli { public static Command InternalReportinstallsuccess() => Create.Command( - "internal-reportinstallsuccess", "internal only", + "internal-reportinstallsuccess", + "", Accept.ExactlyOneArgument()); } } \ No newline at end of file From b24e9f4cec893f8068384975c30dfe56d179a451 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Wed, 16 May 2018 14:29:53 -0700 Subject: [PATCH 3/3] Add unit tests for `dotnet complete`. This commit adds a few simple unit tests to cover the `dotnet complete` command. It only checks the top-level output, integration with the `new` command from the templating engine, and the custom `nuget` command parser that is solely intended for use with `dotnet complete`. --- .../dotnet-complete/CompleteCommand.cs | 12 +- .../CommandTests/CompleteCommandTests.cs | 194 ++++++++++++++++++ 2 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 test/dotnet.Tests/CommandTests/CompleteCommandTests.cs diff --git a/src/dotnet/commands/dotnet-complete/CompleteCommand.cs b/src/dotnet/commands/dotnet-complete/CompleteCommand.cs index a6061d37d..e3571f13b 100644 --- a/src/dotnet/commands/dotnet-complete/CompleteCommand.cs +++ b/src/dotnet/commands/dotnet-complete/CompleteCommand.cs @@ -12,6 +12,16 @@ namespace Microsoft.DotNet.Cli { public static int Run(string[] args) { + return RunWithReporter(args, Reporter.Output); + } + + public static int RunWithReporter(string [] args, IReporter reporter) + { + if (reporter == null) + { + throw new ArgumentNullException(nameof(reporter)); + } + try { DebugHelper.HandleDebugSwitch(ref args); @@ -28,7 +38,7 @@ namespace Microsoft.DotNet.Cli foreach (var suggestion in suggestions) { - Console.WriteLine(suggestion); + reporter.WriteLine(suggestion); } } catch (Exception) diff --git a/test/dotnet.Tests/CommandTests/CompleteCommandTests.cs b/test/dotnet.Tests/CommandTests/CompleteCommandTests.cs new file mode 100644 index 000000000..c9323ea45 --- /dev/null +++ b/test/dotnet.Tests/CommandTests/CompleteCommandTests.cs @@ -0,0 +1,194 @@ +// 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.IO; +using System.Linq; +using FluentAssertions; +using Microsoft.DotNet.Cli; +using Microsoft.DotNet.Tools.Test.Utilities; +using Xunit; + +namespace Microsoft.DotNet.Tests.Commands +{ + public class CompleteCommandTests : TestBase + { + [Fact] + public void GivenOnlyDotnetItSuggestsTopLevelCommandsAndOptions() + { + var expected = new string[] { + "--diagnostics", + "--help", + "--info", + "--list-runtimes", + "--list-sdks", + "--version", + "-d", + "-h", + "add", + "build", + "build-server", + "clean", + "help", + "list", + "migrate", + "msbuild", + "new", + "nuget", + "pack", + "publish", + "remove", + "restore", + "run", + "sln", + "store", + "test", + "tool", + "vstest" + }; + + var reporter = new BufferedReporter(); + CompleteCommand.RunWithReporter(new[] { "dotnet " }, reporter).Should().Be(0); + reporter.Lines.Should().Equal(expected.OrderBy(c => c)); + } + + [Fact] + public void GivenASlashItSuggestsTopLevelOptions() + { + var expected = new string[] { + "--diagnostics", + "--help", + "--info", + "--list-runtimes", + "--list-sdks", + "--version", + "-d", + "-h", + "build-server" // This should be removed when completion is based on "starts with" rather than "contains". + // See https://github.com/dotnet/cli/issues/8958. + }; + + var reporter = new BufferedReporter(); + CompleteCommand.RunWithReporter(new[] { "dotnet -" }, reporter).Should().Be(0); + reporter.Lines.Should().Equal(expected.OrderBy(c => c)); + } + + [Fact] + public void GivenNewCommandItDisplaysCompletions() + { + var expected = new string[] { + "--force", + "--help", + "--install", + "--language", + "--list", + "--name", + "--nuget-source", + "--output", + "--type", + "--uninstall", + "-h", + "-i", + "-l", + "-lang", + "-n", + "-o", + "-u" + }; + + var reporter = new BufferedReporter(); + CompleteCommand.RunWithReporter(new[] { "dotnet new " }, reporter).Should().Be(0); + reporter.Lines.Should().Equal(expected.OrderBy(c => c)); + } + + [Fact] + public void GivenNuGetCommandItDisplaysCompletions() + { + var expected = new string[] { + "--help", + "--verbosity", + "--version", + "-h", + "-v", + "delete", + "locals", + "push", + }; + + var reporter = new BufferedReporter(); + CompleteCommand.RunWithReporter(new[] { "dotnet nuget " }, reporter).Should().Be(0); + reporter.Lines.Should().Equal(expected.OrderBy(c => c)); + } + + [Fact] + public void GivenNuGetDeleteCommandItDisplaysCompletions() + { + var expected = new string[] { + "--api-key", + "--force-english-output", + "--help", + "--no-service-endpoint", + "--non-interactive", + "--source", + "-h", + "-k", + "-s" + }; + + var reporter = new BufferedReporter(); + CompleteCommand.RunWithReporter(new[] { "dotnet nuget delete " }, reporter).Should().Be(0); + reporter.Lines.Should().Equal(expected.OrderBy(c => c)); + } + + [Fact] + public void GivenNuGetLocalsCommandItDisplaysCompletions() + { + var expected = new string[] { + "--clear", + "--force-english-output", + "--help", + "--list", + "-c", + "-h", + "-l", + "all", + "global-packages", + "http-cache", + "temp" + }; + + var reporter = new BufferedReporter(); + CompleteCommand.RunWithReporter(new[] { "dotnet nuget locals " }, reporter).Should().Be(0); + reporter.Lines.Should().Equal(expected.OrderBy(c => c)); + } + + [Fact] + public void GivenNuGetPushCommandItDisplaysCompletions() + { + var expected = new string[] { + "--api-key", + "--disable-buffering", + "--force-english-output", + "--help", + "--no-service-endpoint", + "--no-symbols", + "--source", + "--symbol-api-key", + "--symbol-source", + "--timeout", + "-d", + "-h", + "-k", + "-n", + "-s", + "-sk", + "-ss", + "-t" + }; + + var reporter = new BufferedReporter(); + CompleteCommand.RunWithReporter(new[] { "dotnet nuget push " }, reporter).Should().Be(0); + reporter.Lines.Should().Equal(expected.OrderBy(c => c)); + } + } +}