File: Parser.cs
Web Access
Project: src\src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if CLI_AOT
using System.CommandLine;
using Microsoft.DotNet.Cli.Utils;

namespace Microsoft.DotNet.Cli;

public static class Parser
{
    internal static RootCommand RootCommand { get; } = CreateCommand();

    private static RootCommand CreateCommand()
    {
        var versionOption = new Option<bool>("--version") { Description = "Display .NET SDK version." };
        var infoOption = new Option<bool>("--info") { Description = "Display .NET information." };

        var rootCommand = new RootCommand("The .NET CLI")
        {
            versionOption,
            infoOption,
        };

        rootCommand.SetAction(parseResult =>
        {
            if (parseResult.GetValue(versionOption))
            {
                CommandLineInfo.PrintVersion();
                return 0;
            }
            if (parseResult.GetValue(infoOption))
            {
                CommandLineInfo.PrintInfo();
                return 0;
            }
            parseResult.InvocationConfiguration.Output.WriteLine("Usage: dn [options]");
            return 0;
        });

        return rootCommand;
    }

    public static ParseResult Parse(string[] args) => RootCommand.Parse(args);

    public static int Invoke(ParseResult parseResult) => parseResult.Invoke();
}

#else
using System.CommandLine;
using System.CommandLine.Help;
using System.CommandLine.StaticCompletions;
using System.Reflection;
using Microsoft.DotNet.Cli.Commands;
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.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.Test;
using Microsoft.DotNet.Cli.Commands.Tool;
using Microsoft.DotNet.Cli.Commands.Tool.Store;
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.Help;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.TemplateEngine.Cli;
using Command = System.CommandLine.Command;

namespace Microsoft.DotNet.Cli;

public static class Parser
{
    private static DotNetCommandDefinition CreateCommand()
    {
        var rootCommand = new DotNetCommandDefinition();

        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 HelpOption helpOption)
            {
                helpOption.Action = new PrintHelpAction(helpOption, DotnetHelpBuilder.Instance.Value);
                option.Description = CliStrings.ShowHelpDescription;
            }
        }

        // Augment the definition of each subcommand with command-specific actions and completions.
        AddCommandParser.ConfigureCommand(rootCommand.AddCommand);
        BuildCommandParser.ConfigureCommand(rootCommand.BuildCommand);
        BuildServerCommandParser.ConfigureCommand(rootCommand.BuildServerCommand);
        CleanCommandParser.ConfigureCommand(rootCommand.CleanCommand);
        DnxCommandParser.ConfigureCommand(rootCommand.DnxCommand);
        FormatCommandParser.ConfigureCommand(rootCommand.FormatCommand);
        CompleteCommandParser.ConfigureCommand(rootCommand.CompleteCommand);
        FsiCommandParser.ConfigureCommand(rootCommand.FsiCommand);
        ListCommandParser.ConfigureCommand(rootCommand.ListCommand);
        MSBuildCommandParser.ConfigureCommand(rootCommand.MSBuildCommand);

        // Currently `new` command implementation replaces the definition entirely:
        rootCommand.Subcommands[rootCommand.Subcommands.IndexOf(rootCommand.NewCommand)] = NewCommandParser.ConfigureCommand(rootCommand.NewCommand);

        // TODO: https://github.com/dotnet/sdk/issues/52661
        // https://github.com/NuGet/NuGet.Client/blob/bf048eb714eb6b1912ba868edca4c7cfec454841/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs
        // Add `why` subcommand to the definition instead.
        var nugetCommand = rootCommand.NuGetCommand;
        NuGet.CommandLine.XPlat.Commands.Why.WhyCommand.GetWhyCommand(nugetCommand, NuGetVirtualProjectBuilder.Instance);

        NuGetCommandParser.ConfigureCommand(nugetCommand);

        PackCommandParser.ConfigureCommand(rootCommand.PackCommand);
        PackageCommandParser.ConfigureCommand(rootCommand.PackageCommand);
        ParseCommandParser.ConfigureCommand(rootCommand.ParseCommand);
        ProjectCommandParser.ConfigureCommand(rootCommand.ProjectCommand);
        PublishCommandParser.ConfigureCommand(rootCommand.PublishCommand);
        ReferenceCommandParser.ConfigureCommand(rootCommand.ReferenceCommand);
        RemoveCommandParser.ConfigureCommand(rootCommand.RemoveCommand);
        RestoreCommandParser.ConfigureCommand(rootCommand.RestoreCommand);
        RunCommandParser.ConfigureCommand(rootCommand.RunCommand);
        RunApiCommandParser.ConfigureCommand(rootCommand.RunApiCommand);
        SolutionCommandParser.ConfigureCommand(rootCommand.SolutionCommand);
        StoreCommandParser.ConfigureCommand(rootCommand.StoreCommand);
        TestCommandParser.ConfigureCommand(rootCommand.TestCommand);
        ToolCommandParser.ConfigureCommand(rootCommand.ToolCommand);
        VSTestCommandParser.ConfigureCommand(rootCommand.VSTestCommand);
        HelpCommandParser.ConfigureCommand(rootCommand.HelpCommand);
        SdkCommandParser.ConfigureCommand(rootCommand.SdkCommand);
        InternalReportInstallSuccessCommandParser.ConfigureCommand(rootCommand.InternalReportInstallSuccessCommand);
        WorkloadCommandParser.ConfigureCommand(rootCommand.WorkloadCommand);
        CompletionsCommandParser.ConfigureCommand(rootCommand.CompletionsCommand);

        rootCommand.DiagOption.Action = new HandleDiagnosticAction(rootCommand.DiagOption);
        rootCommand.VersionOption.Action = new PrintVersionAction(rootCommand.VersionOption);
        rootCommand.InfoOption.Action = new PrintInfoAction(rootCommand.InfoOption);
        rootCommand.CliSchemaOption.Action = new PrintCliSchemaAction(rootCommand.CliSchemaOption);

        // TODO: https://github.com/dotnet/sdk/issues/52661
        // https://github.com/NuGet/NuGet.Client/blob/bf048eb714eb6b1912ba868edca4c7cfec454841/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs
        // Add `package` subcommands to the definition instead.
        NuGet.CommandLine.XPlat.NuGetCommands.Add(rootCommand, CommonOptions.CreateInteractiveOption(acceptArgument: true), NuGetVirtualProjectBuilder.Instance);

        rootCommand.SetAction(parseResult =>
        {
            if (parseResult.GetValue(rootCommand.DiagOption) && parseResult.Tokens.Count == 1)
            {
                // When user does not specify any args except of diagnostics ("dotnet -d"),
                // we do nothing as HandleDiagnosticAction 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) =>
        RootCommand.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>
    internal static DotNetCommandDefinition RootCommand { get; } = CreateCommand();

    /// <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 is not null && exception.GetType().Name.Equals("WorkloadManifestCompositionException"))
        {
            Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ?
                exception.ToString().Red().Bold() :
                exception.Message.Red().Bold());
        }
        else if (exception is not null)
        {
            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 (IsInNuGetCommandTree(command))
            {
                NuGetCommand.Run(context.ParseResult);
            }
            else if (command is MSBuildCommandDefinition)
            {
                new MSBuildForwardingApp(MSBuildArgs.ForHelp).Execute();
                context.Output.WriteLine();
                additionalOption(context);
            }
            else if (command is VSTestCommandDefinition)
            {
                new VSTestForwardingApp(helpArgs).Execute();
            }
            else if (command is FormatCommandDefinition format)
            {
                var arguments = context.ParseResult.GetValue(format.Arguments) ?? [];
                new FormatForwardingApp([.. arguments, .. helpArgs]).Execute();
            }
            else if (command is FsiCommandDefinition)
            {
                new FsiForwardingApp(helpArgs).Execute();
            }
            else if (command is ICustomHelp helpCommand)
            {
                var blocks = helpCommand.CustomHelpLayout();
                foreach (var block in blocks)
                {
                    block(context);
                }
            }
            else
            {
                // TODO: avoid modifying the commands:
                // https://github.com/dotnet/sdk/issues/52136

                if (command.Name.Equals(ListReferenceCommandDefinition.Name))
                {
                    Command? listCommand = command.Parents.Single() as Command;
                    if (listCommand is not null)
                    {
                        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] = ListCommandDefinition.CreateSlnOrProjectArgument(CliStrings.ProjectArgumentName, CliStrings.ProjectArgumentDescription);
                            }
                        }
                    }
                }
                else if (command.Name.Equals(AddPackageCommandDefinition.Name) || command.Name.Equals(AddCommandDefinition.Name))
                {
                    // Don't show package completions in help
                    foreach (var argument in command.Arguments)
                    {
                        argument.CompletionSources.Clear();
                    }
                }
                else if (command is WorkloadSearchCommandDefinition workloadSearchCommand)
                {
                    // Set shorter description for displaying parent command help.
                    workloadSearchCommand.VersionCommand.Description = CliStrings.ShortWorkloadSearchVersionDescription;
                }

                base.Write(context);
            }
        }

        private static bool IsInNuGetCommandTree(Command command)
        {
            Command? current = command;
            while (current is not null)
            {
                if (current is NuGetCommandDefinition)
                {
                    return true;
                }
                current = current.Parents.FirstOrDefault(p => p is Command) as Command;
            }
            return false;
        }
    }
}
#endif