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-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/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 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/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)); + } + } +} 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