| File: Invocation\ParseErrorAction.cs | Web Access |
| Project: 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; }