|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System.CommandLine;
using System.CommandLine.Completions;
using System.CommandLine.Invocation;
using System.Reflection;
using Microsoft.DotNet.Cli.Commands.Build;
using Microsoft.DotNet.Cli.Commands.BuildServer;
using Microsoft.DotNet.Cli.Commands.Clean;
using Microsoft.DotNet.Cli.Commands.Dnx;
using Microsoft.DotNet.Cli.Commands.Format;
using Microsoft.DotNet.Cli.Commands.Fsi;
using Microsoft.DotNet.Cli.Commands.Help;
using Microsoft.DotNet.Cli.Commands.Hidden.Add;
using Microsoft.DotNet.Cli.Commands.Hidden.Add.Package;
using Microsoft.DotNet.Cli.Commands.Hidden.Complete;
using Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess;
using Microsoft.DotNet.Cli.Commands.Hidden.List;
using Microsoft.DotNet.Cli.Commands.Hidden.List.Reference;
using Microsoft.DotNet.Cli.Commands.Hidden.Parse;
using Microsoft.DotNet.Cli.Commands.Hidden.Remove;
using Microsoft.DotNet.Cli.Commands.MSBuild;
using Microsoft.DotNet.Cli.Commands.New;
using Microsoft.DotNet.Cli.Commands.NuGet;
using Microsoft.DotNet.Cli.Commands.Pack;
using Microsoft.DotNet.Cli.Commands.Package;
using Microsoft.DotNet.Cli.Commands.Package.Add;
using Microsoft.DotNet.Cli.Commands.Project;
using Microsoft.DotNet.Cli.Commands.Publish;
using Microsoft.DotNet.Cli.Commands.Reference;
using Microsoft.DotNet.Cli.Commands.Restore;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Commands.Run.Api;
using Microsoft.DotNet.Cli.Commands.Sdk;
using Microsoft.DotNet.Cli.Commands.Solution;
using Microsoft.DotNet.Cli.Commands.Store;
using Microsoft.DotNet.Cli.Commands.Test;
using Microsoft.DotNet.Cli.Commands.Tool;
using Microsoft.DotNet.Cli.Commands.VSTest;
using Microsoft.DotNet.Cli.Commands.Workload;
using Microsoft.DotNet.Cli.Commands.Workload.Search;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.TemplateEngine.Cli;
using Microsoft.TemplateEngine.Cli.Help;
using Command = System.CommandLine.Command;
namespace Microsoft.DotNet.Cli;
public static class Parser
{
public static readonly Command InstallSuccessCommand = InternalReportInstallSuccessCommandParser.GetCommand();
// Subcommands
public static readonly Command[] Subcommands =
[
AddCommandParser.GetCommand(),
BuildCommandParser.GetCommand(),
BuildServerCommandParser.GetCommand(),
CleanCommandParser.GetCommand(),
DnxCommandParser.GetCommand(),
FormatCommandParser.GetCommand(),
CompleteCommandParser.GetCommand(),
FsiCommandParser.GetCommand(),
ListCommandParser.GetCommand(),
MSBuildCommandParser.GetCommand(),
NewCommandParser.GetCommand(),
NuGetCommandParser.GetCommand(),
PackCommandParser.GetCommand(),
PackageCommandParser.GetCommand(),
ParseCommandParser.GetCommand(),
ProjectCommandParser.GetCommand(),
PublishCommandParser.GetCommand(),
ReferenceCommandParser.GetCommand(),
RemoveCommandParser.GetCommand(),
RestoreCommandParser.GetCommand(),
RunCommandParser.GetCommand(),
RunApiCommandParser.GetCommand(),
SolutionCommandParser.GetCommand(),
StoreCommandParser.GetCommand(),
TestCommandParser.GetCommand(),
ToolCommandParser.GetCommand(),
VSTestCommandParser.GetCommand(),
HelpCommandParser.GetCommand(),
SdkCommandParser.GetCommand(),
InstallSuccessCommand,
WorkloadCommandParser.GetCommand(),
new System.CommandLine.StaticCompletions.CompletionsCommand()
];
public static readonly Option<bool> DiagOption = CommonOptionsFactory.CreateDiagnosticsOption(recursive: false);
public static readonly Option<bool> VersionOption = new("--version")
{
Arity = ArgumentArity.Zero
};
public static readonly Option<bool> InfoOption = new("--info")
{
Arity = ArgumentArity.Zero
};
public static readonly Option<bool> ListSdksOption = new("--list-sdks")
{
Arity = ArgumentArity.Zero
};
public static readonly Option<bool> ListRuntimesOption = new("--list-runtimes")
{
Arity = ArgumentArity.Zero
};
public static readonly Option<bool> CliSchemaOption = new("--cli-schema")
{
Description = CliStrings.SDKSchemaCommandDefinition,
Arity = ArgumentArity.Zero,
Recursive = true,
Hidden = true,
Action = new PrintCliSchemaAction()
};
// Argument
public static readonly Argument<string> DotnetSubCommand = new("subcommand") { Arity = ArgumentArity.ZeroOrOne, Hidden = true };
private static RootCommand ConfigureCommandLine(RootCommand rootCommand)
{
for (int i = rootCommand.Options.Count - 1; i >= 0; i--)
{
Option option = rootCommand.Options[i];
if (option is VersionOption)
{
rootCommand.Options.RemoveAt(i);
}
else if (option is System.CommandLine.Help.HelpOption helpOption)
{
helpOption.Action = new DotnetHelpAction()
{
Builder = DotnetHelpBuilder.Instance.Value
};
option.Description = CliStrings.ShowHelpDescription;
}
}
// Add subcommands
foreach (var subcommand in Subcommands)
{
rootCommand.Subcommands.Add(subcommand);
}
// Add options
rootCommand.Options.Add(DiagOption);
rootCommand.Options.Add(VersionOption);
rootCommand.Options.Add(InfoOption);
rootCommand.Options.Add(ListSdksOption);
rootCommand.Options.Add(ListRuntimesOption);
rootCommand.Options.Add(CliSchemaOption);
// Add argument
rootCommand.Arguments.Add(DotnetSubCommand);
// NuGet implements several commands in its own repo. Add them to the .NET SDK via the provided API.
NuGet.CommandLine.XPlat.NuGetCommands.Add(rootCommand, CommonOptions.InteractiveOption(acceptArgument: true));
rootCommand.SetAction(parseResult =>
{
if (parseResult.GetValue(DiagOption) && parseResult.Tokens.Count == 1)
{
// when user does not specify any args except of diagnostics ("dotnet -d"), we do nothing
// as Program.ProcessArgs already enabled the diagnostic output
return 0;
}
else
{
// when user does not specify any args (just "dotnet"), a usage needs to be printed
parseResult.InvocationConfiguration.Output.WriteLine(CliUsage.HelpText);
return 0;
}
});
return rootCommand;
}
public static Command GetBuiltInCommand(string commandName) =>
Subcommands.FirstOrDefault(c => c.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase));
/// <summary>
/// Implements token-per-line response file handling for the CLI. We use this instead of the built-in S.CL handling
/// to ensure backwards-compatibility with MSBuild.
/// </summary>
public static bool TokenPerLine(string tokenToReplace, out IReadOnlyList<string> replacementTokens, out string errorMessage)
{
var filePath = Path.GetFullPath(tokenToReplace);
if (File.Exists(filePath))
{
var lines = File.ReadAllLines(filePath);
var trimmedLines =
lines
// Remove content in the lines that start with # after trimmer leading whitespace
.Select(line => line.TrimStart().StartsWith('#') ? string.Empty : line)
// trim leading/trailing whitespace to not pass along dead spaces
.Select(x => x.Trim())
// Remove empty lines
.Where(line => line.Length > 0);
replacementTokens = [.. trimmedLines];
errorMessage = null;
return true;
}
else
{
replacementTokens = null;
errorMessage = string.Format(CliStrings.ResponseFileNotFound, tokenToReplace);
return false;
}
}
public static ParserConfiguration ParserConfiguration { get; } = new()
{
EnablePosixBundling = false,
ResponseFileTokenReplacer = TokenPerLine
};
public static InvocationConfiguration InvocationConfiguration { get; } = new()
{
EnableDefaultExceptionHandler = false,
};
/// <summary>
/// The root command for the .NET CLI.
/// </summary>
/// <remarks>
/// If you use this Command directly, you _must_ use <see cref="ParserConfiguration"/>
/// and <see cref="InvocationConfiguration"/> to ensure that the command line parser
/// and invoker are configured correctly.
/// </remarks>
public static RootCommand RootCommand { get; } = ConfigureCommandLine(new()
{
Directives = { new DiagramDirective(), new SuggestDirective(), new EnvironmentVariablesDirective() }
});
/// <summary>
/// You probably want to use <see cref="Parse(string[])"/> instead of this method.
/// This has to internally split the string into an array of arguments
/// before parsing, which is not as efficient as using the array overload.
/// And also won't always split tokens the way the user will expect on their shell.
/// </summary>
public static ParseResult Parse(string commandLineUnsplit) => RootCommand.Parse(commandLineUnsplit, ParserConfiguration);
public static ParseResult Parse(string[] args) => RootCommand.Parse(args, ParserConfiguration);
public static int Invoke(ParseResult parseResult) => parseResult.Invoke(InvocationConfiguration);
public static Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default) => parseResult.InvokeAsync(InvocationConfiguration, cancellationToken);
public static int Invoke(string[] args) => Invoke(Parse(args));
public static Task<int> InvokeAsync(string[] args, CancellationToken cancellationToken = default) => InvokeAsync(Parse(args), cancellationToken);
internal static int ExceptionHandler(Exception exception, ParseResult parseResult)
{
if (exception is TargetInvocationException)
{
exception = exception.InnerException;
}
if (exception is GracefulException)
{
Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ?
exception.ToString().Red().Bold() :
exception.Message.Red().Bold());
}
else if (exception is CommandParsingException)
{
Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ?
exception.ToString().Red().Bold() :
exception.Message.Red().Bold());
parseResult.ShowHelp();
}
else if (exception.GetType().Name.Equals("WorkloadManifestCompositionException"))
{
Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ?
exception.ToString().Red().Bold() :
exception.Message.Red().Bold());
}
else
{
Reporter.Error.Write("Unhandled exception: ".Red().Bold());
Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ?
exception.ToString().Red().Bold() :
exception.Message.Red().Bold());
}
return 1;
}
internal class DotnetHelpBuilder : HelpBuilder
{
private DotnetHelpBuilder(int maxWidth = int.MaxValue) : base(maxWidth) { }
public static Lazy<HelpBuilder> Instance = new(() =>
{
int windowWidth;
try
{
windowWidth = Console.WindowWidth;
}
catch
{
windowWidth = int.MaxValue;
}
DotnetHelpBuilder dotnetHelpBuilder = new(windowWidth);
return dotnetHelpBuilder;
});
public static void additionalOption(HelpContext context)
{
List<TwoColumnHelpRow> options = [];
HashSet<Option> uniqueOptions = [];
foreach (Option option in context.Command.Options)
{
if (!option.Hidden && uniqueOptions.Add(option))
{
options.Add(context.HelpBuilder.GetTwoColumnRow(option, context));
}
}
if (options.Count <= 0)
{
return;
}
context.Output.WriteLine(CliStrings.MSBuildAdditionalOptionTitle);
context.HelpBuilder.WriteColumns(options, context);
context.Output.WriteLine();
}
public override void Write(HelpContext context)
{
var command = context.Command;
var helpArgs = new string[] { "--help" };
// custom help overrides
if (command.Equals(RootCommand))
{
Console.Out.WriteLine(CliUsage.HelpText);
return;
}
// argument/option cleanups specific to help
foreach (var option in command.Options)
{
option.EnsureHelpName();
}
if (command.Equals(NuGetCommandParser.GetCommand()) || command.Parents.Any(parent => parent == NuGetCommandParser.GetCommand()))
{
NuGetCommand.Run(context.ParseResult);
}
else if (command.Name.Equals(MSBuildCommandParser.GetCommand().Name))
{
new MSBuildForwardingApp(MSBuildArgs.ForHelp).Execute();
context.Output.WriteLine();
additionalOption(context);
}
else if (command.Name.Equals(VSTestCommandParser.GetCommand().Name))
{
new VSTestForwardingApp(helpArgs).Execute();
}
else if (command.Name.Equals(FormatCommandParser.GetCommand().Name))
{
var arguments = context.ParseResult.GetValue(FormatCommandParser.Arguments);
new FormatForwardingApp([.. arguments, .. helpArgs]).Execute();
}
else if (command.Name.Equals(FsiCommandParser.GetCommand().Name))
{
new FsiForwardingApp(helpArgs).Execute();
}
else if (command is TemplateEngine.Cli.Commands.ICustomHelp helpCommand)
{
var blocks = helpCommand.CustomHelpLayout();
foreach (var block in blocks)
{
block(context);
}
}
else if (command.Name.Equals(FormatCommandParser.GetCommand().Name))
{
new FormatForwardingApp(helpArgs).Execute();
}
else if (command.Name.Equals(FsiCommandParser.GetCommand().Name))
{
new FsiForwardingApp(helpArgs).Execute();
}
else
{
if (command.Name.Equals(ListReferenceCommandParser.GetCommand().Name))
{
Command listCommand = command.Parents.Single() as Command;
for (int i = 0; i < listCommand.Arguments.Count; i++)
{
if (listCommand.Arguments[i].Name == CliStrings.SolutionOrProjectArgumentName)
{
// Name is immutable now, so we create a new Argument with the right name..
listCommand.Arguments[i] = ListCommandParser.CreateSlnOrProjectArgument(CliStrings.ProjectArgumentName, CliStrings.ProjectArgumentDescription);
}
}
}
else if (command.Name.Equals(AddPackageCommandParser.GetCommand().Name) || command.Name.Equals(AddCommandParser.GetCommand().Name))
{
// Don't show package completions in help
PackageAddCommandParser.CmdPackageArgument.CompletionSources.Clear();
}
else if (command.Name.Equals(WorkloadSearchCommandParser.GetCommand().Name))
{
// Set shorter description for displaying parent command help.
WorkloadSearchVersionsCommandParser.GetCommand().Description = CliStrings.ShortWorkloadSearchVersionDescription;
}
base.Write(context);
}
}
}
private class PrintCliSchemaAction : SynchronousCommandLineAction
{
public override bool Terminating => true;
public override int Invoke(ParseResult parseResult)
{
CliSchema.PrintCliSchema(parseResult.CommandResult, parseResult.InvocationConfiguration.Output, Program.TelemetryClient);
return 0;
}
}
}
|