File: Extensions\ParseResultExtensions.cs
Web Access
Project: ..\..\..\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.CommandLine.Parsing;
using System.Diagnostics;
using System.Text.RegularExpressions;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using CommandResult = System.CommandLine.Parsing.CommandResult;
 
namespace Microsoft.DotNet.Cli.Extensions;
 
public static class ParseResultExtensions
{
    ///<summary>
    /// Finds the command of the parse result and invokes help for that command.
    /// If no command is specified, invokes help for the application.
    ///<summary>
    ///<remarks>
    /// This is accomplished by finding a set of tokens that should be valid and appending a help token
    /// to that list, then re-parsing the list of tokens. This is not ideal - either we should have a direct way
    /// of invoking help for a ParseResult, or we should eliminate this custom, ad-hoc help invocation by moving
    /// more situations that want to show help into Parsing Errors (which trigger help in the default System.CommandLine pipeline)
    /// or custom Invocation Middleware, so we can more easily create our version of a HelpResult type.
    ///</remarks>
    public static void ShowHelp(this ParseResult parseResult)
    {
        // take from the start of the list until we hit an option/--/unparsed token
        // since commands can have arguments, we must take those as well in order to get accurate help
        Parser.Parse([
            ..parseResult.Tokens.TakeWhile(token => token.Type == TokenType.Argument || token.Type == TokenType.Command || token.Type == TokenType.Directive).Select(t => t.Value),
            "-h"
        ]).Invoke();
    }
 
    public static void ShowHelpOrErrorIfAppropriate(this ParseResult parseResult)
    {
        if (parseResult.Errors.Any())
        {
            var unrecognizedTokenErrors = parseResult.Errors.Where(error =>
            {
                // Can't really cache this access in a static or something because it implicitly depends on the environment.
                var rawResourcePartsForThisLocale = DistinctFormatStringParts(CliStrings.UnrecognizedCommandOrArgument);
                return ErrorContainsAllParts(error.Message, rawResourcePartsForThisLocale);
            });
            if (parseResult.CommandResult.Command.TreatUnmatchedTokensAsErrors ||
                parseResult.Errors.Except(unrecognizedTokenErrors).Any())
            {
                throw new CommandParsingException(
                    message: string.Join(Environment.NewLine,
                                         parseResult.Errors.Select(e => e.Message)),
                    parseResult: parseResult);
            }
        }
 
        ///<summary>Splits a .NET format string by the format placeholders (the {N} parts) to get an array of the literal parts, to be used in message-checking</summary>
        static string[] DistinctFormatStringParts(string formatString)
        {
            return Regex.Split(formatString, @"{[0-9]+}"); // match the literal '{', followed by any of 0-9 one or more times, followed by the literal '}'
        }
 
 
        /// <summary>given a string and a series of parts, ensures that all parts are present in the string in sequential order</summary>
        static bool ErrorContainsAllParts(ReadOnlySpan<char> error, string[] parts)
        {
            foreach (var part in parts)
            {
                var foundIndex = error.IndexOf(part);
                if (foundIndex != -1)
                {
                    error = error.Slice(foundIndex + part.Length);
                    continue;
                }
                else
                {
                    return false;
                }
            }
            return true;
        }
    }
 
    public static string RootSubCommandResult(this ParseResult parseResult)
    {
        return parseResult.RootCommandResult.Children?
            .Select(child => GetSymbolResultValue(parseResult, child))
            .FirstOrDefault(subcommand => !string.IsNullOrEmpty(subcommand)) ?? string.Empty;
    }
 
    public static bool IsDotnetBuiltInCommand(this ParseResult parseResult)
    {
        return string.IsNullOrEmpty(parseResult.RootSubCommandResult()) ||
            Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null;
    }
 
    public static bool IsTopLevelDotnetCommand(this ParseResult parseResult)
    {
        return parseResult.CommandResult.Command.Equals(Parser.RootCommand) && string.IsNullOrEmpty(parseResult.RootSubCommandResult());
    }
 
    public static bool CanBeInvoked(this ParseResult parseResult)
    {
        return Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null ||
            parseResult.Tokens.Any(token => token.Type == TokenType.Directive) ||
            (parseResult.IsTopLevelDotnetCommand() && string.IsNullOrEmpty(parseResult.GetValue(Parser.DotnetSubCommand)));
    }
 
    public static int HandleMissingCommand(this ParseResult parseResult)
    {
        Reporter.Error.WriteLine(CliStrings.RequiredCommandNotPassed.Red());
        parseResult.ShowHelp();
        return 1;
    }
 
    public static string[] GetArguments(this ParseResult parseResult)
    {
        return parseResult.Tokens.Select(t => t.Value)
            .ToArray()
            .GetSubArguments();
    }
 
    public static string[] GetSubArguments(this string[] args)
    {
        var subargs = args.ToList();
 
        // Don't remove any arguments that are being passed to the app in dotnet run
        var dashDashIndex = subargs.IndexOf("--");
 
        var runArgs = dashDashIndex > -1 ? subargs.GetRange(dashDashIndex, subargs.Count() - dashDashIndex) : [];
        subargs = dashDashIndex > -1 ? subargs.GetRange(0, dashDashIndex) : subargs;
 
        return
        [
            .. subargs
                .SkipWhile(arg => Parser.DiagOption.Name.Equals(arg) || Parser.DiagOption.Aliases.Contains(arg) || arg.Equals("dotnet"))
                .Skip(1), // remove top level command (ex build or publish)
            .. runArgs
        ];
    }
 
    public static bool DiagOptionPrecedesSubcommand(this string[] args, string subCommand)
    {
        if (string.IsNullOrEmpty(subCommand))
        {
            return true;
        }
 
        for (var i = 0; i < args.Length; i++)
        {
            if (args[i].Equals(subCommand))
            {
                return false;
            }
            else if (Parser.DiagOption.Name.Equals(args) || Parser.DiagOption.Aliases.Contains(args[i]))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static string? GetSymbolResultValue(ParseResult parseResult, SymbolResult symbolResult) => symbolResult switch
    {
        CommandResult commandResult => commandResult.Command.Name,
        ArgumentResult argResult => argResult.Tokens.FirstOrDefault()?.Value,
        _ => parseResult.GetResult(Parser.DotnetSubCommand)?.GetValueOrDefault<string>()
    };
 
    public static bool BothArchAndOsOptionsSpecified(this ParseResult parseResult) =>
        (parseResult.HasOption(CommonOptions.ArchitectureOption) ||
        parseResult.HasOption(CommonOptions.LongFormArchitectureOption)) &&
        parseResult.HasOption(CommonOptions.OperatingSystemOption);
 
    public static bool UsingRunCommandShorthandProjectOption(this ParseResult parseResult)
    {
        if (parseResult.HasOption(RunCommandParser.PropertyOption) && parseResult.GetValue(RunCommandParser.PropertyOption)!.Any())
        {
            var projVals = parseResult.GetRunCommandShorthandProjectValues();
            if (projVals?.Any() is true)
            {
                if (projVals.Count() != 1 || parseResult.HasOption(RunCommandParser.ProjectOption))
                {
                    throw new GracefulException(CliStrings.OnlyOneProjectAllowed);
                }
                return true;
            }
        }
        return false;
    }
 
    public static IEnumerable<string>? GetRunCommandShorthandProjectValues(this ParseResult parseResult)
    {
        var properties = GetRunPropertyOptions(parseResult, true);
        return properties?.Where(property => !property.Contains("="));
    }
 
    public static IEnumerable<string> GetRunCommandPropertyValues(this ParseResult parseResult)
    {
        var shorthandProperties = GetRunPropertyOptions(parseResult, true)?.Where(property => property.Contains("="));
        var longhandProperties = GetRunPropertyOptions(parseResult, false);
        return (shorthandProperties, longhandProperties) switch
        {
            (null, null) => Enumerable.Empty<string>(),
            (null, var longhand) => longhand,
            (var shorthand, null) => shorthand,
            (var shorthand, var longhand) => shorthand.Concat(longhand)
        };
    }
 
    private static IEnumerable<string>? GetRunPropertyOptions(ParseResult parseResult, bool shorthand)
    {
        var optionString = shorthand ? "-p" : "--property";
        var propertyOptions = parseResult.CommandResult.Children.Where(c => GetOptionTokenOrDefault(c)?.Value.Equals(optionString) ?? false);
        var propertyValues = propertyOptions.SelectMany(o => o.Tokens.Select(t => t.Value)).ToArray();
        return propertyValues;
 
        static Token? GetOptionTokenOrDefault(SymbolResult symbolResult)
        {
            if (symbolResult is not OptionResult optionResult)
            {
                return null;
            }
 
            return optionResult.IdentifierToken ?? new Token($"--{optionResult.Option.Name}", TokenType.Option, optionResult.Option);
        }
    }
 
    [Conditional("DEBUG")]
    public static void HandleDebugSwitch(this ParseResult parseResult)
    {
        if (parseResult.HasOption(CommonOptions.DebugOption))
        {
            DebugHelper.WaitForDebugger();
        }
    }
 
    /// <summary>
    /// Only returns the value for this option if the option is present and there are no parse errors for that option.
    /// This allows cross-cutting code like the telemetry filters to safely get the value without throwing on null-ref errors.
    /// If you are inside a command handler or 'normal' System.CommandLine code then you don't need this - the parse error handling
    /// will have covered these cases.
    /// </summary>
    public static T? SafelyGetValueForOption<T>(this ParseResult parseResult, Option<T> optionToGet)
    {
        if (parseResult.GetResult(optionToGet) is OptionResult optionResult &&
            !parseResult.Errors.Any(e => e.SymbolResult == optionResult))
        {
            return optionResult.GetValue(optionToGet);
        }
        else
        {
            return default;
        }
    }
    public static T? SafelyGetValueForOption<T>(this ParseResult parseResult, string name)
    {
        if (parseResult.GetResult(name) is OptionResult optionResult && // only return a value if there _is_ a value - default or otherwise
            !parseResult.Errors.Any(e => e.SymbolResult == optionResult) // only return a value if this isn't a parsing error
            && optionResult.Option.ValueType.IsAssignableTo(typeof(T))) // only return a value if coercing the type won't error
        {
            // shouldn't happen because of the above checks, but we can be safe since this is only used in telemetry, and should
            // be resistant to errors
            try
            {
                return optionResult.GetValue<T>(name);
            }
            catch
            {
                return default;
            }
        }
        else
        {
            return default;
        }
    }
 
    /// <summary>
    /// Checks if the option is present and not implicit (i.e. not set by default).
    /// This is useful for checking if the user has explicitly set an option, as opposed to it being set by default.
    /// </summary>
    public static bool HasOption(this ParseResult parseResult, Option option) => parseResult.GetResult(option) is OptionResult or && !or.Implicit;
 
    /// <summary>
    /// Checks if the option with given name is present and not implicit (i.e. not set by default).
    /// This is useful for checking if the user has explicitly set an option, as opposed to it being set by default.
    /// </summary>
    public static bool HasOption(this ParseResult parseResult, string name)
        => parseResult.GetResult(name) is OptionResult or && !or.Implicit;
 
    /// <summary>
    /// Checks if the option is present and not implicit (i.e. not set by default).
    /// This is useful for checking if the user has explicitly set an option, as opposed to it being set by default.
    /// </summary>
    public static bool HasOption(this SymbolResult symbolResult, Option option) => symbolResult.GetResult(option) is OptionResult or && !or.Implicit;
 
    /// <summary>
    /// Checks if the option with given name is present and not implicit (i.e. not set by default).
    /// This is useful for checking if the user has explicitly set an option, as opposed to it being set by default.
    /// </summary>
    public static bool HasOption(this SymbolResult symbolResult, string name)
        => symbolResult.GetResult(name) is OptionResult or && !or.Implicit;
}