File: Invocation\ParseErrorAction.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.Help;
using System.CommandLine.Parsing;
using System.Linq;
using System.Threading;

namespace System.CommandLine.Invocation;

/// <summary>
/// Provides command-line output with error details in the case of a parsing error.
/// </summary>
public sealed class ParseErrorAction : SynchronousCommandLineAction
{
    /// <summary>
    /// Gets or sets a value that indicates whether to show help along with error details when an error is found during parsing.
    /// </summary>
    /// <value><see langword="true" /> to show help along with parse error details. <see langword="false" /> to not show help.</value>
    public bool ShowHelp { get; set; } = true;

    /// <summary>
    /// Gets or sets a value that indicates whether to show typo suggestions along with error details when an error is found during parsing.
    /// </summary>
    /// <value><see langword="true" /> to show suggestions along with parse error details. <see langword="false" /> to now show suggestions.</value>
    public bool ShowTypoCorrections { get; set; } = true;

    /// <inheritdoc />
    public override int Invoke(ParseResult parseResult)
    {
        if (ShowTypoCorrections)
        {
            WriteTypoCorrectionSuggestions(parseResult);
        }

        WriteErrorDetails(parseResult);

        if (ShowHelp)
        {
            WriteHelp(parseResult);
        }

        return 1;
    }

    private static void WriteErrorDetails(ParseResult parseResult)
    {
        ConsoleHelpers.ResetTerminalForegroundColor();
        ConsoleHelpers.SetTerminalForegroundRed();

        var stdErr = parseResult.InvocationConfiguration.Error;

        foreach (var error in parseResult.Errors)
        {
            stdErr.WriteLine(error.Message);
        }

        stdErr.WriteLine();

        ConsoleHelpers.ResetTerminalForegroundColor();
    }

    private static void WriteHelp(ParseResult parseResult)
    {
        // Find the most proximate help option (if any) and invoke its action.
        var availableHelpOptions =
            parseResult
                .CommandResult
                .RecurseWhileNotNull(r => r.Parent as CommandResult)
                .Select(r => r.Command.Options.OfType<HelpOption>().FirstOrDefault());

        if (availableHelpOptions.FirstOrDefault(o => o is not null) is { Action: not null } helpOption)
        {
            switch (helpOption.Action)
            {
                case SynchronousCommandLineAction syncAction:
                    syncAction.Invoke(parseResult);
                    break;

                case AsynchronousCommandLineAction asyncAction:
                    asyncAction.InvokeAsync(parseResult, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
                    break;
            }
        }
    }

    private static void WriteTypoCorrectionSuggestions(ParseResult parseResult)
    {
        var unmatchedTokens = parseResult.UnmatchedTokens;

        for (var i = 0; i < unmatchedTokens.Count; i++)
        {
            var token = unmatchedTokens[i];

            bool first = true;
            foreach (string suggestion in GetPossibleTokens(parseResult.CommandResult, token))
            {
                if (first)
                {
                    parseResult.InvocationConfiguration.Output.WriteLine(LocalizationResources.SuggestionsTokenNotMatched(token));
                    first = false;
                }

                parseResult.InvocationConfiguration.Output.WriteLine(suggestion);
            }
        }

        if (unmatchedTokens.Count != 0)
        {
            parseResult.InvocationConfiguration.Output.WriteLine();
        }

        static IEnumerable<string> GetPossibleTokens(CommandResult commandResult, string token)
        {
            Command targetSymbol = commandResult.Command;

            // Collect symbols from the current command's children (options + subcommands)
            // plus Recursive options from every ancestor command.
            IEnumerable<Symbol> candidates = targetSymbol.HasOptions || targetSymbol.HasSubcommands
                ? targetSymbol.Children.Where(x => !x.Hidden && x is Option or Command)
                : Enumerable.Empty<Symbol>();

            for (var parent = commandResult.Parent as CommandResult; parent is not null; parent = parent.Parent as CommandResult)
            {
                candidates = candidates.Concat(
                    parent.Command.Options.Where(o => o.Recursive && !o.Hidden));
            }

            IEnumerable<string> possibleMatches = candidates
                                                  .Select(symbol =>
                                                  {
                                                      AliasSet? aliasSet = symbol is Option option ? option._aliases : ((Command)symbol)._aliases;

                                                      if (aliasSet is null)
                                                      {
                                                          return symbol.Name;
                                                      }

                                                      return new[] { symbol.Name }.Concat(aliasSet)
                                                                                  .OrderBy(x => GetDistance(token, x))
                                                                                  .ThenByDescending(x => GetStartsWithDistance(token, x))
                                                                                  .First();
                                                  });

            int? bestDistance = null;
            return possibleMatches
                   .Select(possibleMatch => (possibleMatch, distance: GetDistance(token, possibleMatch)))
                   .Where(tuple => tuple.distance <= MaxLevenshteinDistance)
                   .OrderBy(tuple => tuple.distance)
                   .ThenByDescending(tuple => GetStartsWithDistance(token, tuple.possibleMatch))
                   .TakeWhile(tuple =>
                   {
                       var (_, distance) = tuple;
                       if (bestDistance is null)
                       {
                           bestDistance = distance;
                       }

                       return distance == bestDistance;
                   })
                   .Select(tuple => tuple.possibleMatch);
        }

        static int GetStartsWithDistance(string first, string second)
        {
            int i;
            for (i = 0; i < first.Length && i < second.Length && first[i] == second[i]; i++)
            {
            }

            return i;
        }

        //Based on https://blogs.msdn.microsoft.com/toub/2006/05/05/generic-levenshtein-edit-distance-with-c/
        static int GetDistance(string first, string second)
        {
            // Validate parameters
            if (first is null)
            {
                throw new ArgumentNullException(nameof(first));
            }

            if (second is null)
            {
                throw new ArgumentNullException(nameof(second));
            }

            // Get the length of both.  If either is 0, return
            // the length of the other, since that number of insertions
            // would be required.

            int n = first.Length, m = second.Length;
            if (n == 0) return m;
            if (m == 0) return n;

            // Rather than maintain an entire matrix (which would require O(n*m) space),
            // just store the current row and the next row, each of which has a length m+1,
            // so just O(m) space. Initialize the current row.

            int curRow = 0, nextRow = 1;
            int[][] rows = { new int[m + 1], new int[m + 1] };

            for (int j = 0; j <= m; ++j)
            {
                rows[curRow][j] = j;
            }

            // For each virtual row (since we only have physical storage for two)
            for (int i = 1; i <= n; ++i)
            {
                // Fill in the values in the row
                rows[nextRow][0] = i;
                for (int j = 1; j <= m; ++j)
                {
                    int dist1 = rows[curRow][j] + 1;
                    int dist2 = rows[nextRow][j - 1] + 1;
                    int dist3 = rows[curRow][j - 1] + (first[i - 1].Equals(second[j - 1]) ? 0 : 1);

                    rows[nextRow][j] = Math.Min(dist1, Math.Min(dist2, dist3));
                }

                // Swap the current and next rows
                if (curRow == 0)
                {
                    curRow = 1;
                    nextRow = 0;
                }
                else
                {
                    curRow = 0;
                    nextRow = 1;
                }
            }

            // Return the computed edit distance
            return rows[curRow][m];
        }
    }

    private const int MaxLevenshteinDistance = 3;
}