File: Commands\Why\WhyCommand.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.CommandLine.XPlat\NuGet.CommandLine.XPlat.csproj (NuGet.CommandLine.XPlat)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Help;
using System.CommandLine.Parsing;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.CommandLineUtils;
using NuGet.Common;
using Spectre.Console;

namespace NuGet.CommandLine.XPlat.Commands.Why
{
    public static class WhyCommand
    {
        internal static void Register(CommandLineApplication app)
        {
            app.Command("why", whyCmd =>
            {
                whyCmd.Description = Strings.WhyCommand_Description;
            });
        }

        internal static void Register(Command rootCommand, Lazy<IAnsiConsole> console, IVirtualProjectBuilder? virtualProjectBuilder = null)
        {
            Register(rootCommand, console,
                () => new WhyCommandRunner(new MSBuildAPIUtility(NullLogger.Instance, virtualProjectBuilder)));
        }

        /// <summary>
        /// This is a temporary API until NuGet migrates all our commands to System.CommandLine, at which time I suspect we'll have a NuGetParser.GetNuGetCommand for all the `dotnet nuget *` commands.
        /// For now, this allows the dotnet CLI to invoke why directly, instead of running NuGet.CommandLine.XPlat as a child process.
        /// </summary>
        /// <param name="rootCommand">The <c>dotnet nuget</c> command handler, to add <c>why</c> to.</param>
        /// <param name="virtualProjectBuilder">For handling file-based apps.</param>
        public static void GetWhyCommand(Command rootCommand, IVirtualProjectBuilder? virtualProjectBuilder = null)
        {
            Register(rootCommand,
                new Lazy<IAnsiConsole>(() => Spectre.Console.AnsiConsole.Console),
                virtualProjectBuilder);
        }

        // For binary backcompat. To delete once the SDK starts using the other overload.
        public static void GetWhyCommand(Command rootCommand)
        {
            GetWhyCommand(rootCommand, virtualProjectBuilder: null);
        }

        internal static void Register(Command rootCommand, Lazy<IAnsiConsole> console, Func<WhyCommandRunner> getCommandRunner)
        {
            Register(rootCommand, console, action: (args) => getCommandRunner().ExecuteCommand(args));
        }

        // console must be lazy, because Spectre.Console's AnsiConsole will send VT sequences to the output
        // as soon as it's created, which causes problems with the dotnet CLI's C++ output, like dotnet --info
        internal static void Register(Command rootCommand, Lazy<IAnsiConsole> console, Func<WhyCommandArgs, Task<int>> action)
        {
            var whyCommand = new DocumentedCommand("why", Strings.WhyCommand_Description, "https://aka.ms/dotnet/nuget/why");

            Argument<string> path = new Argument<string>("PROJECT|SOLUTION|FILE")
            {
                Description = Strings.WhyCommand_PathArgument_Description,
                // We really want this to be zero or one, however, because this is the first argument, it doesn't work.
                // Instead, we need to use a CustomParser to choose if the argument is the path, or the package.
                // In order for the parser to tell us there's more than 1 argument available, we need to tell Argument
                // that it supports more than one, but then in the custom parser we'll make sure we only take at most 1.
                Arity = ArgumentArity.ZeroOrMore,
                CustomParser = ar =>
                {
                    if (HasPathArgument(ar))
                    {
                        var value = ar.Tokens[0];
                        ar.OnlyTake(1);
                        return value.Value;
                    }

                    ar.OnlyTake(0);
                    var currentDirectory = Directory.GetCurrentDirectory();
                    return currentDirectory;

                    bool HasPathArgument(ArgumentResult ar)
                    {
                        // If there's only one argument, it could be the path, or the package.
                        if (ar.Tokens.Count == 1)
                        {
                            var value = ar.Tokens[0].Value;
                            return File.Exists(value) || Directory.Exists(value);
                        }

                        return ar.Tokens.Count > 1;
                    }
                }
            };

            Argument<string> package = new Argument<string>("PACKAGE")
            {
                Description = Strings.WhyCommand_PackageArgument_Description,
                Arity = ArgumentArity.ExactlyOne
            };

            Option<List<string>> frameworks = new Option<List<string>>("--framework", "-f")
            {
                Description = Strings.WhyCommand_FrameworksOption_Description,
                Arity = ArgumentArity.OneOrMore
            };

            HelpOption help = new HelpOption()
            {
                Arity = ArgumentArity.Zero
            };

            whyCommand.Arguments.Add(path);
            whyCommand.Arguments.Add(package);
            whyCommand.Options.Add(frameworks);
            whyCommand.Options.Add(help);

            whyCommand.SetAction(async (parseResult, cancellationToken) =>
            {
                try
                {
                    var whyCommandArgs = new WhyCommandArgs
                    {
                        Path = parseResult.GetValue(path)!,
                        Package = parseResult.GetValue(package)!,
                        Frameworks = parseResult.GetValue(frameworks)!,
                        Logger = console.Value,
                        CancellationToken = cancellationToken,
                        DotnetVersionChecker = DotnetVersionChecker.Instance,
                    };

                    int exitCode = await action(whyCommandArgs);
                    return exitCode;
                }
                catch (ArgumentException ex)
                {
                    console.Value.Markup($"[red]{ex.Message}[/]");
                    return ExitCodes.InvalidArguments;
                }
            });

            rootCommand.Subcommands.Add(whyCommand);
        }
    }
}