File: Parsing\ParseDiagramAction.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;
using System.CommandLine.Binding;
using System.CommandLine.Invocation;
using System.Linq;
using System.Text;

namespace System.CommandLine.Parsing
{
    /// <summary>
    /// Implements the <c>[diagram]</c> directive action, which when specified on the command line will short circuit normal command handling and display a diagram explaining the parse result for the command line input.
    /// </summary>
    internal sealed class ParseDiagramAction : SynchronousCommandLineAction
    {
        private readonly int _parseErrorReturnValue;

        internal ParseDiagramAction(int parseErrorReturnValue) => _parseErrorReturnValue = parseErrorReturnValue;

        public override int Invoke(ParseResult parseResult)
        {
            parseResult.InvocationConfiguration.Output.WriteLine(Diagram(parseResult));
            return parseResult.Errors.Count == 0 ? 0 : _parseErrorReturnValue;
        }
        
        /// <summary>
        /// Formats a string explaining a parse result.
        /// </summary>
        /// <param name="parseResult">The parse result to be diagrammed.</param>
        /// <returns>A string containing a diagram of the parse result.</returns>
        internal static StringBuilder Diagram(ParseResult parseResult)
        {
            var builder = new StringBuilder(100);

            Diagram(builder, parseResult.RootCommandResult, parseResult);

            var unmatchedTokens = parseResult.UnmatchedTokens;
            if (unmatchedTokens.Count > 0)
            {
                builder.Append("   ???-->");

                for (var i = 0; i < unmatchedTokens.Count; i++)
                {
                    var error = unmatchedTokens[i];
                    builder.Append(' ');
                    builder.Append(error);
                }
            }

            return builder;
        }

        private static void Diagram(
            StringBuilder builder,
            SymbolResult symbolResult,
            ParseResult parseResult)
        {
            if (parseResult.Errors.Any(e => e.SymbolResult == symbolResult))
            {
                builder.Append('!');
            }

            switch (symbolResult)
            {
                case DirectiveResult { Directive: not DiagramDirective }:
                    break;

                case ArgumentResult argumentResult:
                {
                    var includeArgumentName =
                        argumentResult.Argument.FirstParent!.Symbol is Command { HasArguments: true, Arguments.Count: > 1 };

                    if (includeArgumentName)
                    {
                        builder.Append("[ ");
                        builder.Append(argumentResult.Argument.Name);
                        builder.Append(' ');
                    }

                    if (argumentResult.Argument.Arity.MaximumNumberOfValues > 0)
                    {
                        ArgumentConversionResult conversionResult = argumentResult.GetArgumentConversionResult();
                        switch (conversionResult.Result)
                        {
                            case ArgumentConversionResultType.NoArgument:
                                break;
                            case ArgumentConversionResultType.Successful:
                                switch (conversionResult.Value)
                                {
                                    case string s:
                                        builder.Append($"<{s}>");
                                        break;
                                
                                    case IEnumerable items:
                                        builder.Append('<');
                                        builder.Append(
                                            string.Join("> <",
                                                        items.Cast<object>().ToArray()));
                                        builder.Append('>');
                                        break;

                                    default:
                                        builder.Append('<');
                                        builder.Append(conversionResult.Value);
                                        builder.Append('>');
                                        break;
                                }

                                break;

                            default: // failures
                                builder.Append('<');
                                builder.Append(string.Join("> <", symbolResult.Tokens.Select(t => t.Value)));
                                builder.Append('>');

                                break;
                        }
                    }

                    if (includeArgumentName)
                    {
                        builder.Append(" ]");
                    }

                    break;
                }

                default:
                {
                    OptionResult? optionResult = symbolResult as OptionResult;

                    if (optionResult is { Implicit: true })
                    {
                        builder.Append('*');
                    }

                    builder.Append("[ ");

                    if (optionResult is not null)
                    {
                        builder.Append(optionResult.IdentifierToken?.Value ?? optionResult.Option.Name);
                    }
                    else
                    {
                        builder.Append(((CommandResult)symbolResult).IdentifierToken.Value);
                    }

                    foreach (SymbolResult child in symbolResult.SymbolResultTree.GetChildren(symbolResult))
                    {
                        if (child is ArgumentResult arg &&
                            (arg.Argument.ValueType == typeof(bool) ||
                             arg.Argument.Arity.MaximumNumberOfValues == 0))
                        {
                            continue;
                        }

                        builder.Append(' ');

                        Diagram(builder, child, parseResult);
                    }

                    builder.Append(" ]");
                    break;
                }
            }
        }
    }
}