| File: src\sdk\src\Cli\dotnet\Extensions\ParseResultExtensions.cs | Web Access |
| Project: src\sdk\src\Cli\dotnet-aot\dotnet-aot.csproj (dotnet-aot) |
// 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.CommandLine; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.ProjectTools; using CommandResult = System.CommandLine.Parsing.CommandResult; namespace Microsoft.DotNet.Cli.Extensions; public static class ParseResultExtensions { public static string RootSubCommandResult(this ParseResult parseResult) => parseResult.RootCommandResult.Children? .Select(child => parseResult.GetSymbolResultValue(child)) .FirstOrDefault(subcommand => !string.IsNullOrEmpty(subcommand)) ?? string.Empty; public static bool IsTopLevelDotnetCommand(this ParseResult parseResult) => parseResult.CommandResult.Command.Equals(Parser.RootCommand) && string.IsNullOrEmpty(parseResult.RootSubCommandResult()); /// <summary> /// Returns true when the parse result is an unrecognized top-level token that did not match a /// built-in command and so landed on the root's hidden subcommand argument - e.g. an external /// command (<c>dotnet ef</c>) or an implicit file-based app (<c>dotnet app.cs</c>). /// </summary> /// <remarks> /// The managed CLI resolves these via external command resolution or its file-based run pipeline /// (see <c>Program.ExecuteExternalCommand</c>/<c>TryRunFileBasedApp</c>). The NativeAOT entry /// point cannot do either, so it uses this to defer such invocations to the managed CLI rather /// than running the root command's usage action. /// </remarks> public static bool RequiresManagedCommandResolution(this ParseResult parseResult) => parseResult.CommandResult.Command.Equals(Parser.RootCommand) && !string.IsNullOrEmpty(parseResult.GetValue(Parser.RootCommand.DotnetSubCommand)); /// <summary> /// Detects whether this parse result looks like an implicit file-based app invocation /// (e.g. <c>dotnet app.cs ...</c>), where the only unmatched token is a first argument that /// resolves to a valid C# entry-point path. Returns the matching token, or <see langword="null"/>. /// </summary> /// <remarks> /// This detection is shared between the managed CLI - which re-dispatches these invocations as /// <c>dotnet run --file app.cs</c> (see <c>Program.TryRunFileBasedApp</c>) - and the NativeAOT /// entry point, which cannot run file-based apps itself and so defers them to the managed CLI. /// </remarks> public static Token? GetFileBasedAppEntryPointToken(this ParseResult parseResult) => parseResult.GetResult(Parser.RootCommand.DotnetSubCommand) is { Tokens: [{ Type: TokenType.Argument, Value: { } } unmatchedCommandOrFile] } && IsValidEntryPointPath(unmatchedCommandOrFile.Value) ? unmatchedCommandOrFile : null; // duplicated from VirtualProjectBuilder to temporarily avoid MSBuild dlls on AOT codepath private static bool IsValidEntryPointPath(string entryPointFilePath) { if (!File.Exists(entryPointFilePath)) { return false; } if (entryPointFilePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) { return true; } // Check if the first two characters are #! try { using var stream = File.OpenRead(entryPointFilePath); int first = stream.ReadByte(); int second = stream.ReadByte(); return first == '#' && second == '!'; } catch { return false; } } private static string? GetSymbolResultValue(this ParseResult parseResult, SymbolResult symbolResult) => symbolResult switch { CommandResult commandResult => commandResult.Command.Name, ArgumentResult argResult => argResult.Tokens.FirstOrDefault()?.Value, _ => parseResult.GetResult(Parser.RootCommand.DotnetSubCommand)?.GetValueOrDefault<string>() }; /// <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. var filteredTokenValues = parseResult.Tokens.TakeWhile(token => token.Type == TokenType.Argument || token.Type == TokenType.Command || token.Type == TokenType.Directive) .Select(t => t.Value); Parser.Parse([.. filteredTokenValues, "-h"]).Invoke(); } public static string[] GetArguments(this ParseResult parseResult) => 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; // Remove top level command (ex build or publish). var subargsFiltered = subargs .SkipWhile(arg => Parser.RootCommand.DiagOption.Name.Equals(arg) || Parser.RootCommand.DiagOption.Aliases.Contains(arg) || arg.Equals("dotnet")) .Skip(1); return [.. subargsFiltered, .. runArgs]; } public static bool CanBeInvoked(this ParseResult parseResult) => Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null || parseResult.Tokens.Any(token => token.Type == TokenType.Directive) || (parseResult.IsTopLevelDotnetCommand() && string.IsNullOrEmpty(parseResult.GetValue(Parser.RootCommand.DotnetSubCommand))); public static bool IsDotnetBuiltInCommand(this ParseResult parseResult) => string.IsNullOrEmpty(parseResult.RootSubCommandResult()) || Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null; 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) => // Match the literal '{', followed by any of 0-9 one or more times, followed by the literal '}'. Regex.Split(formatString, @"{[0-9]+}"); /// <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; } return false; } return true; } } public static int HandleMissingCommand(this ParseResult parseResult) { Reporter.Error.WriteLine(CliStrings.RequiredCommandNotPassed.Red()); parseResult.ShowHelp(); return 1; } public static IEnumerable<string>? GetRunCommandShorthandProjectValues(this ParseResult parseResult) => parseResult.GetRunPropertyOptions(true)?.Where(property => !property.Contains("=")); public static IEnumerable<string> GetRunCommandPropertyValues(this ParseResult parseResult) { var shorthandProperties = parseResult.GetRunPropertyOptions(true)?.Where(property => property.Contains("=")); var longhandProperties = parseResult.GetRunPropertyOptions(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(this 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); } } #if !CLI_AOT [Conditional("DEBUG")] public static void HandleDebugSwitch(this ParseResult parseResult) { if (parseResult.HasOption(CommonOptions.DebugOption)) { DebugHelper.WaitForDebugger(); } } #endif public static string GetCommandName(this ParseResult parseResult) { // Walk the parent command tree to find the top-level command name and get the full command name for this ParseResult. List<string> parentNames = [parseResult.CommandResult.Command.Name]; var current = parseResult.CommandResult.Parent; while (current is CommandResult parentCommandResult) { parentNames.Add(parentCommandResult.Command.Name); current = parentCommandResult.Parent; } parentNames.Reverse(); // Options that perform terminating actions are considered part of the command name as they are essentially subcommands themselves. // Example: dotnet --version if (parseResult.Action is InvocableOptionAction { Terminating: true } optionAction) { parentNames.Add(optionAction.Option.Name); } return string.Join(' ', parentNames); } }