File: CommandInvocation.cs
Web Access
Project: 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.

using System.CommandLine;
using System.Diagnostics;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;
using CommandResult = System.CommandLine.Parsing.CommandResult;

namespace Microsoft.DotNet.Cli;

/// <summary>
///  Shared command-invocation logic used by both the managed CLI entry point (<see cref="Program"/>)
///  and the NativeAOT bridge (<c>NativeEntryPoint</c>). Keeping this in a single type guarantees the
///  two entry points stay in parity instead of each maintaining their own copy of the invocation and
///  exit-code handling.
/// </summary>
internal static class CommandInvocation
{
    /// <summary>
    ///  Invokes a built-in command in-process via <see cref="Parser.Invoke(ParseResult)"/>, applying the
    ///  same exit-code handling on both the managed CLI and the NativeAOT bridge (including the "new"
    ///  command's 127 adjustment and <see cref="Parser.ExceptionHandler"/>). Errors are reported and
    ///  converted to an exit code, except that under NativeAOT a <see cref="CommandNotAvailableInAotException"/>
    ///  is allowed to propagate so the bridge can fall back to hosting the managed CLI.
    /// </summary>
    internal static int ExecuteInternalCommand(ParseResult parseResult)
    {
        Debug.Assert(parseResult.CanBeInvoked());
        int exitCode;
        using var _ = Activities.Source.StartActivity("invocation");
        try
        {
            exitCode = Parser.Invoke(parseResult);
            if (parseResult.Errors.Any())
            {
                exitCode = AdjustExitCodeForNew();
            }
        }
#if CLI_AOT
        catch (CommandNotAvailableInAotException)
        {
            // The parsed command is parse-only under NativeAOT and must run in the managed CLI. Let the
            // native entry point catch this and fall back to hosting dotnet.dll.
            throw;
        }
#endif
        catch (Exception exception)
        {
            exitCode = Parser.ExceptionHandler(exception, parseResult);
        }
        return exitCode;

        int AdjustExitCodeForNew()
        {
            var commandResult = parseResult.CommandResult;
            while (commandResult is not null)
            {
                if (commandResult.Command.Name == "new")
                {
                    // Default parse error exit code is 1.
                    // For the "new" command and its subcommands, it needs to be 127.
                    return 127;
                }
                commandResult = commandResult.Parent as CommandResult;
            }
            return exitCode;
        }
    }
}