File: ParseResult.cs
Web Access
Project: src\src\command-line-api\src\System.CommandLine\System.CommandLine.csproj (System.CommandLine)
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.CommandLine.Completions;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;

namespace System.CommandLine
{
    /// <summary>
    /// Describes the results of parsing a command line input based on a specific parser configuration.
    /// </summary>
    public sealed class ParseResult
    {
        private readonly CommandResult _rootCommandResult;
        private readonly IReadOnlyList<Token> _unmatchedTokens;
        private CompletionContext? _completionContext;
        private readonly CommandLineAction? _action;
        private readonly List<CommandLineAction>? _preActions;
        private InvocationConfiguration? _invocationConfiguration;

        internal ParseResult(
            ParserConfiguration configuration,
            CommandResult rootCommandResult,
            CommandResult commandResult,
            List<Token> tokens,
            List<Token>? unmatchedTokens,
            List<ParseError>? errors,
            string? commandLineText = null,
            CommandLineAction? action = null,
            List<CommandLineAction>? preActions = null)
        {
            Configuration = configuration;
            _rootCommandResult = rootCommandResult;
            CommandResult = commandResult;
            _action = action;
            _preActions = preActions;

            // skip the root command when populating Tokens property
            if (tokens.Count > 1)
            {
                // Since TokenizeResult.Tokens is not public and not used anywhere after the parsing,
                // we take advantage of its mutability and remove the root command token
                // instead of creating a copy of the whole list.
                tokens.RemoveAt(0);
                Tokens = tokens;
            }
            else
            {
                Tokens = [];
            }

            CommandLineText = commandLineText;
            _unmatchedTokens = unmatchedTokens is null ? Array.Empty<Token>() : unmatchedTokens;
            Errors = errors is not null ? errors : Array.Empty<ParseError>();
        }

        internal static ParseResult Empty() => new RootCommand().Parse(Array.Empty<string>());

        /// <summary>
        /// Gets a result indicating the command specified in the command line input.
        /// </summary>
        public CommandResult CommandResult { get; }

        /// <summary>
        /// Gets the configuration used to produce the parse result.
        /// </summary>
        public ParserConfiguration Configuration { get; }

        /// <summary>
        /// Gets the configuration used to specify command line run-time behavior.
        /// </summary>
        public InvocationConfiguration InvocationConfiguration
        {
            get => _invocationConfiguration ??= new();
            private set => _invocationConfiguration = value;
        }

        /// <summary>
        /// Gets the root command result.
        /// </summary>
        public CommandResult RootCommandResult => _rootCommandResult;

        /// <summary>
        /// Gets the parse errors found while parsing command-line input.
        /// </summary>
        public IReadOnlyList<ParseError> Errors { get; }

        /// <summary>
        /// Gets the tokens identified while parsing command-line input.
        /// </summary>
        public IReadOnlyList<Token> Tokens { get; }

        /// <summary>
        /// Gets the value of a complete command line input prior to splitting and tokenization, when provided.
        /// </summary>
        /// <remarks>This value will not be set when the parser is called from <c>Program.Main</c>. It is primarily used when calculating suggestions via the <c>dotnet-suggest</c> tool.</remarks>
        internal string? CommandLineText { get; }

        /// <summary>
        /// Gets the list of tokens used on the command line that were not matched by the parser.
        /// </summary>
        public IReadOnlyList<string> UnmatchedTokens
            => _unmatchedTokens.Count == 0 ? Array.Empty<string>() : _unmatchedTokens.Select(t => t.Value).ToArray();

        /// <summary>
        /// Gets the completion context for the parse result.
        /// </summary>
        public CompletionContext GetCompletionContext() =>
            _completionContext ??=
                CommandLineText is null
                    ? new CompletionContext(this)
                    : new TextCompletionContext(this, CommandLineText);

        /// <summary>
        /// Gets the parsed or default value for the specified argument.
        /// </summary>
        /// <param name="argument">The argument for which to get a value.</param>
        /// <returns>The parsed value or a configured default.</returns>
        public T? GetValue<T>(Argument<T> argument)
            => RootCommandResult.GetValue(argument);

        /// <summary>
        /// Gets the parsed or default value for the specified option.
        /// </summary>
        /// <param name="option">The option for which to get a value.</param>
        /// <returns>The parsed value or a configured default.</returns>
        public T? GetValue<T>(Option<T> option)
            => RootCommandResult.GetValue(option);

        /// <summary>
        /// Gets the parsed or default value for the specified symbol name, in the context of parsed command (not entire symbol tree).
        /// </summary>
        /// <param name="name">The name of the Symbol for which to get a value.</param>
        /// <returns>The parsed value or a configured default.</returns>
        /// <exception cref="InvalidOperationException">Parsing resulted in one or more errors.</exception>
        /// <exception cref="ArgumentException">No symbol was defined for the given name for the parsed command.</exception>
        /// <exception cref="InvalidCastException">The parsed result cannot be cast to <typeparamref name="T"/>.</exception>
        public T? GetValue<T>(string name)
            => RootCommandResult.GetValue<T>(name);

        /// <summary>
        /// Gets the parsed or default value for the specified required argument or throws.
        /// </summary>
        /// <param name="argument">The argument for which to get a value.</param>
        /// <returns>The parsed value or a configured default.</returns>
        /// <exception cref="InvalidOperationException">The required argument was not parsed or has no default value configured.</exception>
        public T GetRequiredValue<T>(Argument<T> argument)
            => RootCommandResult.GetRequiredValue(argument);

        /// <summary>
        /// Gets the parsed or default value for the specified required option or throws.
        /// </summary>
        /// <param name="option">The option for which to get a value.</param>
        /// <returns>The parsed value or a configured default.</returns>
        /// <exception cref="InvalidOperationException">The required option was not parsed or has no default value configured.</exception>
        public T GetRequiredValue<T>(Option<T> option)
            => RootCommandResult.GetRequiredValue(option);

        /// <summary>
        /// Gets the parsed or default value for the specified required symbol name in the context of the parsed command (not entire symbol tree).
        /// </summary>
        /// <param name="name">The name of the required symbol for which to get a value.</param>
        /// <returns>The parsed value or a configured default.</returns>
        /// <exception cref="InvalidOperationException">Parsing resulted in one or more errors, or the required symbol was not parsed or has no default value configured.</exception>
        /// <exception cref="ArgumentException">No symbol was defined for the given name for the parsed command.</exception>
        /// <exception cref="InvalidCastException">The parsed result cannot be cast to <typeparamref name="T"/>.</exception>
        public T GetRequiredValue<T>(string name)
            => RootCommandResult.GetRequiredValue<T>(name);

        /// <inheritdoc />
        public override string ToString() => ParseDiagramAction.Diagram(this).ToString();

        /// <summary>
        /// Gets the result, if any, for the specified argument.
        /// </summary>
        /// <param name="argument">The argument for which to find a result.</param>
        /// <returns>A result for the specified argument, or <see langword="null"/> if it was not provided and no default was configured.</returns>
        public ArgumentResult? GetResult(Argument argument) =>
            _rootCommandResult.GetResult(argument);

        /// <summary>
        /// Gets the result, if any, for the specified command.
        /// </summary>
        /// <param name="command">The command for which to find a result.</param>
        /// <returns>A result for the specified command, or <see langword="null"/> if it was not provided.</returns>
        public CommandResult? GetResult(Command command) =>
            _rootCommandResult.GetResult(command);

        /// <summary>
        /// Gets the result, if any, for the specified option.
        /// </summary>
        /// <param name="option">The option for which to find a result.</param>
        /// <returns>A result for the specified option, or <see langword="null"/> if it was not provided and no default was configured.</returns>
        public OptionResult? GetResult(Option option) =>
            _rootCommandResult.GetResult(option);

        /// <summary>
        /// Gets the result, if any, for the specified directive.
        /// </summary>
        /// <param name="directive">The directive for which to find a result.</param>
        /// <returns>A result for the specified directive, or <see langword="null"/> if it was not provided.</returns>
        public DirectiveResult? GetResult(Directive directive) => _rootCommandResult.GetResult(directive);

        /// <summary>
        /// Gets the result, if any, for the specified symbol.
        /// </summary>
        /// <param name="symbol">The symbol for which to find a result.</param>
        /// <returns>A result for the specified symbol, or <see langword="null"/> if it was not provided and no default was configured.</returns>
        public SymbolResult? GetResult(Symbol symbol)
            => _rootCommandResult.SymbolResultTree.TryGetValue(symbol, out SymbolResult? result) ? result : null;

        /// <summary>
        /// Finds a result for a symbol having the specified name anywhere in the parse tree.
        /// </summary>
        /// <param name="name">The name of the symbol for which to find a result.</param>
        /// <returns>A symbol result if the argument was matched by the parser or has a default value; otherwise, <c>null</c>.</returns>
        public SymbolResult? GetResult(string name) =>
            _rootCommandResult.SymbolResultTree.GetResult(name);

        /// <summary>
        /// Gets completions based on a given parse result.
        /// </summary>
        /// <param name="position">The position at which completions are requested.</param>
        /// <returns>A set of completions for completion.</returns>
        public IEnumerable<CompletionItem> GetCompletions(
            int? position = null)
        {
            SymbolResult currentSymbolResult = SymbolToComplete(position);

            Symbol currentSymbol = currentSymbolResult switch
            {
                ArgumentResult argumentResult => argumentResult.Argument,
                OptionResult optionResult => optionResult.Option,
                DirectiveResult directiveResult => directiveResult.Directive,
                _ => ((CommandResult)currentSymbolResult).Command
            };

            var context = GetCompletionContext();

            if (position is not null &&
                context is TextCompletionContext tcc)
            {
                context = tcc.AtCursorPosition(position.Value);
            }

            var completions = currentSymbol.GetCompletions(context);

            string[] optionsWithArgumentLimitReached = currentSymbolResult is CommandResult commandResult
                                                           ? OptionsWithArgumentLimitReached(commandResult)
                                                           : Array.Empty<string>();

            completions =
                completions.Where(item => optionsWithArgumentLimitReached.All(s => s != item.Label));

            return completions;

            static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) =>
                commandResult
                    .Children
                    .OfType<OptionResult>()
                    .Where(c => c.IsArgumentLimitReached)
                    .Select(o => o.Option)
                    .SelectMany(c => new[] { c.Name }.Concat(c.Aliases))
                    .ToArray();
        }

        /// <summary>
        /// Invokes the appropriate command handler for a parsed command-line input.
        /// </summary>
        /// <param name="configuration">The configuration used to define invocation behaviors.</param>
        /// <param name="cancellationToken">A token that can be used to cancel an invocation.</param>
        /// <returns>A task whose result can be used as a process exit code.</returns>
        public Task<int> InvokeAsync(
            InvocationConfiguration? configuration = null,
            CancellationToken cancellationToken = default)
        {
            if (configuration is not null)
            {
                InvocationConfiguration = configuration;
            }

            return InvocationPipeline.InvokeAsync(this, cancellationToken);
        }

        /// <summary>
        /// Invokes the appropriate command handler for a parsed command-line input.
        /// </summary>
        /// <param name="configuration">The configuration used to define invocation behaviors.</param>
        /// <returns>A value that can be used as a process exit code.</returns>
        public int Invoke(InvocationConfiguration? configuration = null)
        {
            var useAsync = false;

            if (configuration is not null)
            {
                InvocationConfiguration = configuration;
            }

            if (Action is AsynchronousCommandLineAction)
            {
                useAsync = true;
            }
            else if (PreActions is not null)
            {
                for (var i = 0; i < PreActions.Count; i++)
                {
                    var action = PreActions[i];
                    if (action is AsynchronousCommandLineAction)
                    {
                        useAsync = true;
                        break;
                    }
                }
            }

            if (useAsync)
            {
                return InvocationPipeline.InvokeAsync(this, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
            }
            else
            {
                return InvocationPipeline.Invoke(this);
            }
        }

        /// <summary>
        /// Gets the <see cref="CommandLineAction"/> for parsed result. The handler represents the action
        /// that will be performed when the parse result is invoked.
        /// </summary>
        public CommandLineAction? Action => _action ?? CommandResult.Command.Action;

        internal IReadOnlyList<CommandLineAction>? PreActions => _preActions;

        private SymbolResult SymbolToComplete(int? position = null)
        {
            var commandResult = CommandResult;

            var allSymbolResultsForCompletion = AllSymbolResultsForCompletion();

            var currentSymbol = allSymbolResultsForCompletion.Last();

            return currentSymbol;

            IEnumerable<SymbolResult> AllSymbolResultsForCompletion()
            {
                foreach (var item in commandResult.AllSymbolResults())
                {
                    if (item is CommandResult command)
                    {
                        yield return command;
                    }
                    else if (item is OptionResult option)
                    {
                        if (WillAcceptAnArgument(this, position, option))
                        {
                            yield return option;
                        }
                    }
                }
            }

            static bool WillAcceptAnArgument(
                ParseResult parseResult,
                int? position,
                OptionResult optionResult)
            {
                if (optionResult.Implicit)
                {
                    return false;
                }

                if (!optionResult.IsArgumentLimitReached)
                {
                    return true;
                }

                var completionContext = parseResult.GetCompletionContext();

                if (completionContext is TextCompletionContext textCompletionContext)
                {
                    if (position.HasValue)
                    {
                        textCompletionContext = textCompletionContext.AtCursorPosition(position.Value);
                    }

                    if (textCompletionContext.WordToComplete.Length > 0)
                    {
                        var tokenToComplete = parseResult.Tokens.Last(t => t.Value == textCompletionContext.WordToComplete);

                        return optionResult.Tokens.Contains(tokenToComplete);
                    }
                }

                return !optionResult.IsArgumentLimitReached;
            }
        }
    }
}