From f14430644de57e6d6fca6829ffdcb8068ebbc9fc Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 13 Oct 2022 09:55:48 -0700 Subject: [PATCH 1/7] Make the fuzzy searching flexible by passing in the fuzzy matcher --- .../engine/CommandPathSearch.cs | 14 +-- .../engine/CommandSearcher.cs | 62 +++++------- .../engine/GetCommandCommand.cs | 96 +++++++------------ .../engine/Modules/ModuleUtils.cs | 29 +----- .../utils/FuzzyMatch.cs | 40 ++++++-- 5 files changed, 101 insertions(+), 140 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandPathSearch.cs b/src/System.Management.Automation/engine/CommandPathSearch.cs index a5b8a232b23..92a533e6ec0 100644 --- a/src/System.Management.Automation/engine/CommandPathSearch.cs +++ b/src/System.Management.Automation/engine/CommandPathSearch.cs @@ -37,17 +37,17 @@ internal class CommandPathSearch : IEnumerable, IEnumerator /// /// The patterns to search for in the paths. /// - /// - /// Use likely relevant search. + /// + /// The fuzzy matcher to use for fuzzy searching. /// internal CommandPathSearch( string commandName, LookupPathCollection lookupPaths, ExecutionContext context, Collection? acceptableCommandNames, - bool useFuzzyMatch) + FuzzyMatcher? fuzzyMatcher) { - _useFuzzyMatch = useFuzzyMatch; + _fuzzyMatcher = fuzzyMatcher; string[] commandPatterns; if (acceptableCommandNames != null) { @@ -434,13 +434,13 @@ private void GetNewDirectoryResults(string pattern, string directory) // to forcefully use null if pattern is "." if (pattern.Length != 1 || pattern[0] != '.') { - if (_useFuzzyMatch) + if (_fuzzyMatcher is not null) { var files = new List(); var matchingFiles = Directory.EnumerateFiles(directory); foreach (string file in matchingFiles) { - if (FuzzyMatcher.IsFuzzyMatch(Path.GetFileName(file), pattern)) + if (_fuzzyMatcher.IsFuzzyMatch(Path.GetFileName(file), pattern)) { files.Add(file); } @@ -589,7 +589,7 @@ private void GetNewDirectoryResults(string pattern, string directory) private readonly string[] _orderedPathExt; private readonly Collection? _acceptableCommandNames; - private readonly bool _useFuzzyMatch = false; + private readonly FuzzyMatcher? _fuzzyMatcher; #endregion private members } diff --git a/src/System.Management.Automation/engine/CommandSearcher.cs b/src/System.Management.Automation/engine/CommandSearcher.cs index cf798ddabbd..41c76db7000 100644 --- a/src/System.Management.Automation/engine/CommandSearcher.cs +++ b/src/System.Management.Automation/engine/CommandSearcher.cs @@ -24,29 +24,20 @@ internal class CommandSearcher : IEnumerable, IEnumerator - /// - /// The name of the command to look for. - /// - /// - /// Determines which types of commands glob resolution of the name will take place on. - /// - /// - /// The types of commands to look for. - /// - /// - /// The execution context for this engine instance... - /// - /// - /// If is null. - /// - /// - /// If is null or empty. - /// + /// The name of the command to look for. + /// Determines which types of commands glob resolution of the name will take place on. + /// The types of commands to look for. + /// The execution context for this engine instance. + /// The fuzzy matcher to use for fuzzy searching. + /// + /// If is null. + /// If is null or empty. internal CommandSearcher( string commandName, SearchResolutionOptions options, CommandTypes commandTypes, - ExecutionContext context) + ExecutionContext context, + FuzzyMatcher? fuzzyMatcher = null) { Diagnostics.Assert(context != null, "caller to verify context is not null"); Diagnostics.Assert(!string.IsNullOrEmpty(commandName), "caller to verify commandName is valid"); @@ -55,6 +46,7 @@ internal CommandSearcher( _context = context; _commandResolutionOptions = options; _commandTypes = commandTypes; + _fuzzyMatcher = fuzzyMatcher; // Initialize the enumerators this.Reset(); @@ -705,8 +697,7 @@ private static bool checkPath(string path, string commandName) foreach (KeyValuePair aliasEntry in _context.EngineSessionState.GetAliasTable()) { if (aliasMatcher.IsMatch(aliasEntry.Key) || - (_commandResolutionOptions.HasFlag(SearchResolutionOptions.FuzzyMatch) && - FuzzyMatcher.IsFuzzyMatch(aliasEntry.Key, _commandName))) + _fuzzyMatcher?.IsFuzzyMatch(aliasEntry.Key, _commandName) == true) { matchingAliases.Add(aliasEntry.Value); } @@ -785,8 +776,7 @@ private static bool checkPath(string path, string commandName) foreach ((string functionName, FunctionInfo functionInfo) in _context.EngineSessionState.GetFunctionTable()) { if (functionMatcher.IsMatch(functionName) || - (_commandResolutionOptions.HasFlag(SearchResolutionOptions.FuzzyMatch) && - FuzzyMatcher.IsFuzzyMatch(functionName, _commandName))) + _fuzzyMatcher?.IsFuzzyMatch(functionName, _commandName) == true) { matchingFunction.Add(functionInfo); } @@ -1018,10 +1008,8 @@ private static bool ShouldSkipCommandResolutionForConstrainedLanguage(CommandInf { foreach (CmdletInfo cmdlet in cmdletList) { - if (cmdletMatcher != null && - cmdletMatcher.IsMatch(cmdlet.Name) || - (_commandResolutionOptions.HasFlag(SearchResolutionOptions.FuzzyMatch) && - FuzzyMatcher.IsFuzzyMatch(cmdlet.Name, _commandName))) + if (cmdletMatcher?.IsMatch(cmdlet.Name) == true || + _fuzzyMatcher?.IsFuzzyMatch(cmdlet.Name, _commandName) == true) { if (string.IsNullOrEmpty(moduleName) || moduleName.Equals(cmdlet.ModuleName, StringComparison.OrdinalIgnoreCase)) { @@ -1496,6 +1484,11 @@ private static CanDoPathLookupResult CanDoPathLookup(string possiblePath) /// private readonly ExecutionContext _context; + /// + /// The fuzzy matcher to use for fuzzy searching. + /// + private readonly FuzzyMatcher? _fuzzyMatcher; + /// /// A routine to initialize the path searcher... /// @@ -1528,7 +1521,7 @@ private void setupPathSearcher() _context.CommandDiscovery.GetLookupDirectoryPaths(), _context, acceptableCommandNames: null, - useFuzzyMatch: _commandResolutionOptions.HasFlag(SearchResolutionOptions.FuzzyMatch)); + _fuzzyMatcher); } else { @@ -1544,7 +1537,7 @@ private void setupPathSearcher() _context.CommandDiscovery.GetLookupDirectoryPaths(), _context, ConstructSearchPatternsFromName(_commandName, commandDiscovery: true), - useFuzzyMatch: false); + _fuzzyMatcher); } else if (_canDoPathLookupResult == CanDoPathLookupResult.PathIsRooted) { @@ -1568,7 +1561,7 @@ private void setupPathSearcher() directoryCollection, _context, ConstructSearchPatternsFromName(fileName, commandDiscovery: true), - useFuzzyMatch: false); + _fuzzyMatcher); } else { @@ -1608,7 +1601,7 @@ private void setupPathSearcher() directoryCollection, _context, ConstructSearchPatternsFromName(fileName, commandDiscovery: true), - useFuzzyMatch: false); + _fuzzyMatcher); } else { @@ -1727,17 +1720,14 @@ internal enum SearchResolutionOptions CommandNameIsPattern = 0x04, SearchAllScopes = 0x08, - /// Use fuzzy matching. - FuzzyMatch = 0x10, - /// /// Enable searching for cmdlets/functions by abbreviation expansion. /// - UseAbbreviationExpansion = 0x20, + UseAbbreviationExpansion = 0x10, /// /// Enable resolving wildcard in paths. /// - ResolveLiteralThenPathPatterns = 0x40 + ResolveLiteralThenPathPatterns = 0x20 } } diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index 7beb1f32590..eedc4ef9d73 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -345,7 +345,7 @@ public PSTypeName[] ParameterType [Parameter(ParameterSetName = "AllCommandSet")] public uint FuzzyMinimumDistance { get; set; } = 5; - private List _commandScores = new List(); + private FuzzyMatcher _fuzzyMatcher; /// /// Gets or sets the parameter that determines if return cmdlets based on abbreviation expansion. @@ -367,7 +367,10 @@ protected override void BeginProcessing() #if LEGACYTELEMETRY _timer.Start(); #endif - base.BeginProcessing(); + if (UseFuzzyMatching) + { + _fuzzyMatcher = new FuzzyMatcher(FuzzyMinimumDistance); + } if (ShowCommandInfo.IsPresent && Syntax.IsPresent) { @@ -503,18 +506,8 @@ protected override void EndProcessing() private void OutputResultsHelper(IEnumerable results) { - CommandOrigin origin = this.MyInvocation.CommandOrigin; - - if (UseFuzzyMatching) - { - _commandScores = _commandScores - .Where(x => x.Score <= FuzzyMinimumDistance) - .OrderBy(static x => x.Score) - .ToList(); - results = _commandScores.Select(static x => x.Command); - } + CommandOrigin origin = MyInvocation.CommandOrigin; - int count = 0; foreach (CommandInfo result in results) { // Only write the command if it is visible to the requestor @@ -531,31 +524,23 @@ private void OutputResultsHelper(IEnumerable results) WriteObject(syntax); } } + else if (ShowCommandInfo.IsPresent) + { + // Write output as ShowCommandCommandInfo object. + WriteObject(ConvertToShowCommandInfo(result)); + } + else if (UseFuzzyMatching && _fuzzyMatcher.ScoreMap.TryGetValue(result.Name, out int score)) + { + // If the result was retrieved because of fuzzy searching, the attach the matching score. + var obj = new PSObject(result); + obj.Properties.Add(new PSNoteProperty("Score", score)); + WriteObject(obj); + } else { - if (ShowCommandInfo.IsPresent) - { - // Write output as ShowCommandCommandInfo object. - WriteObject( - ConvertToShowCommandInfo(result)); - } - else - { - if (UseFuzzyMatching) - { - PSObject obj = new PSObject(result); - obj.Properties.Add(new PSNoteProperty("Score", _commandScores[count].Score)); - WriteObject(obj); - } - else - { - WriteObject(result); - } - } + WriteObject(result); } } - - count += 1; } #if LEGACYTELEMETRY @@ -784,11 +769,6 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) options |= SearchResolutionOptions.UseAbbreviationExpansion; } - if (UseFuzzyMatching) - { - options |= SearchResolutionOptions.FuzzyMatch; - } - if ((this.CommandType & CommandTypes.Alias) != 0) { options |= SearchResolutionOptions.ResolveAliasPatterns; @@ -861,24 +841,20 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) IEnumerable commands; if (UseFuzzyMatching) { - foreach (var commandScore in System.Management.Automation.Internal.ModuleUtils.GetFuzzyMatchingCommands( + commands = ModuleUtils.GetMatchingCommands( plainCommandName, - this.Context, - this.MyInvocation.CommandOrigin, + Context, + MyInvocation.CommandOrigin, rediscoverImportedModules: true, - moduleVersionRequired: _isFullyQualifiedModuleSpecified)) - { - _commandScores.Add(commandScore); - } - - commands = _commandScores.Select(static x => x.Command).ToList(); + moduleVersionRequired: _isFullyQualifiedModuleSpecified, + fuzzyMatcher: _fuzzyMatcher); } else { - commands = System.Management.Automation.Internal.ModuleUtils.GetMatchingCommands( + commands = ModuleUtils.GetMatchingCommands( plainCommandName, - this.Context, - this.MyInvocation.CommandOrigin, + Context, + MyInvocation.CommandOrigin, rediscoverImportedModules: true, moduleVersionRequired: _isFullyQualifiedModuleSpecified, useAbbreviationExpansion: UseAbbreviationExpansion); @@ -939,12 +915,12 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) private bool FindCommandForName(SearchResolutionOptions options, string commandName, bool isPattern, bool emitErrors, ref int currentCount, out bool isDuplicate) { - CommandSearcher searcher = - new CommandSearcher( - commandName, - options, - this.CommandType, - this.Context); + var searcher = new CommandSearcher( + commandName, + options, + CommandType, + Context, + _fuzzyMatcher); bool resultFound = false; isDuplicate = false; @@ -1030,12 +1006,6 @@ private bool FindCommandForName(SearchResolutionOptions options, string commandN break; } - if (UseFuzzyMatching) - { - int score = FuzzyMatcher.GetDamerauLevenshteinDistance(current.Name, commandName); - _commandScores.Add(new CommandScore(current, score)); - } - _accumulatedResults.Add(current); if (ArgumentList != null) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 8dab2283fc5..639ad15f7e3 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -379,27 +379,6 @@ internal static bool IsOnSystem32ModulePath(string path) #endif } - /// - /// Gets a list of fuzzy matching commands and their scores. - /// - /// Command pattern. - /// Execution context. - /// Command origin. - /// If true, rediscovers imported modules. - /// Specific module version to be required. - /// IEnumerable tuple containing the CommandInfo and the match score. - internal static IEnumerable GetFuzzyMatchingCommands(string pattern, ExecutionContext context, CommandOrigin commandOrigin, bool rediscoverImportedModules = false, bool moduleVersionRequired = false) - { - foreach (CommandInfo command in GetMatchingCommands(pattern, context, commandOrigin, rediscoverImportedModules, moduleVersionRequired, useFuzzyMatching: true)) - { - int score = FuzzyMatcher.GetDamerauLevenshteinDistance(command.Name, pattern); - if (score <= FuzzyMatcher.MinimumDistance) - { - yield return new CommandScore(command, score); - } - } - } - /// /// Gets a list of matching commands. /// @@ -408,10 +387,10 @@ internal static IEnumerable GetFuzzyMatchingCommands(string patter /// Command origin. /// If true, rediscovers imported modules. /// Specific module version to be required. - /// Use fuzzy matching. + /// Fuzzy matcher for fuzzy searching. /// Use abbreviation expansion for matching. /// Returns matching CommandInfo IEnumerable. - internal static IEnumerable GetMatchingCommands(string pattern, ExecutionContext context, CommandOrigin commandOrigin, bool rediscoverImportedModules = false, bool moduleVersionRequired = false, bool useFuzzyMatching = false, bool useAbbreviationExpansion = false) + internal static IEnumerable GetMatchingCommands(string pattern, ExecutionContext context, CommandOrigin commandOrigin, bool rediscoverImportedModules = false, bool moduleVersionRequired = false, FuzzyMatcher fuzzyMatcher = null, bool useAbbreviationExpansion = false) { // Otherwise, if it had wildcards, just return the "AvailableCommand" // type of command info. @@ -449,7 +428,7 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe foreach (KeyValuePair entry in psModule.ExportedCommands) { if (commandPattern.IsMatch(entry.Value.Name) || - (useFuzzyMatching && FuzzyMatcher.IsFuzzyMatch(entry.Value.Name, pattern)) || + fuzzyMatcher?.IsFuzzyMatch(entry.Value.Name, pattern) == true || (useAbbreviationExpansion && string.Equals(pattern, AbbreviateName(entry.Value.Name), StringComparison.OrdinalIgnoreCase))) { CommandInfo current = null; @@ -509,7 +488,7 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe CommandTypes commandTypes = pair.Value; if (commandPattern.IsMatch(commandName) || - (useFuzzyMatching && FuzzyMatcher.IsFuzzyMatch(commandName, pattern)) || + fuzzyMatcher?.IsFuzzyMatch(commandName, pattern) == true || (useAbbreviationExpansion && string.Equals(pattern, AbbreviateName(commandName), StringComparison.OrdinalIgnoreCase))) { bool shouldExportCommand = true; diff --git a/src/System.Management.Automation/utils/FuzzyMatch.cs b/src/System.Management.Automation/utils/FuzzyMatch.cs index 5b21022a6ef..c38dadc7404 100644 --- a/src/System.Management.Automation/utils/FuzzyMatch.cs +++ b/src/System.Management.Automation/utils/FuzzyMatch.cs @@ -1,23 +1,38 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; using System.Globalization; namespace System.Management.Automation { - internal static class FuzzyMatcher + internal class FuzzyMatcher { - public const int MinimumDistance = 5; + private readonly uint _minimumDistance; + internal readonly Dictionary ScoreMap; + + internal FuzzyMatcher(uint minimumDistance = 5) + { + _minimumDistance = minimumDistance; + ScoreMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + } /// /// Determine if the two strings are considered similar. /// - /// The first string to compare. - /// The second string to compare. + /// The candidate string to be compared. + /// The pattern string to be compared with. /// True if the two strings have a distance <= MinimumDistance. - public static bool IsFuzzyMatch(string string1, string string2) + internal bool IsFuzzyMatch(string candidate, string pattern) { - return GetDamerauLevenshteinDistance(string1, string2) <= MinimumDistance; + int score = GetDamerauLevenshteinDistance(candidate, pattern); + if (score <= _minimumDistance) + { + // There could be duplicate command names during the search. + return ScoreMap.TryAdd(candidate, score); + } + + return false; } /// @@ -27,7 +42,7 @@ public static bool IsFuzzyMatch(string string1, string string2) /// The first string to compare. /// The second string to compare. /// The distance value where the lower the value the shorter the distance between the two strings representing a closer match. - public static int GetDamerauLevenshteinDistance(string string1, string string2) + private static int GetDamerauLevenshteinDistance(string string1, string string2) { string1 = string1.ToUpper(CultureInfo.CurrentCulture); string2 = string2.ToUpper(CultureInfo.CurrentCulture); @@ -36,8 +51,15 @@ public static int GetDamerauLevenshteinDistance(string string1, string string2) int[,] matrix = new int[bounds.Height, bounds.Width]; - for (int height = 0; height < bounds.Height; height++) { matrix[height, 0] = height; } - for (int width = 0; width < bounds.Width; width++) { matrix[0, width] = width; } + for (int height = 0; height < bounds.Height; height++) + { + matrix[height, 0] = height; + } + + for (int width = 0; width < bounds.Width; width++) + { + matrix[0, width] = width; + } for (int height = 1; height < bounds.Height; height++) { From 9ba35e136b087ce482038ebf9dfb523a434e9c96 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 13 Oct 2022 10:07:16 -0700 Subject: [PATCH 2/7] Minor changes --- .../engine/CommandSearcher.cs | 8 ++++---- .../engine/Modules/ModuleUtils.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandSearcher.cs b/src/System.Management.Automation/engine/CommandSearcher.cs index 41c76db7000..a1e5a136134 100644 --- a/src/System.Management.Automation/engine/CommandSearcher.cs +++ b/src/System.Management.Automation/engine/CommandSearcher.cs @@ -697,7 +697,7 @@ private static bool checkPath(string path, string commandName) foreach (KeyValuePair aliasEntry in _context.EngineSessionState.GetAliasTable()) { if (aliasMatcher.IsMatch(aliasEntry.Key) || - _fuzzyMatcher?.IsFuzzyMatch(aliasEntry.Key, _commandName) == true) + (_fuzzyMatcher is not null && _fuzzyMatcher.IsFuzzyMatch(aliasEntry.Key, _commandName))) { matchingAliases.Add(aliasEntry.Value); } @@ -776,7 +776,7 @@ private static bool checkPath(string path, string commandName) foreach ((string functionName, FunctionInfo functionInfo) in _context.EngineSessionState.GetFunctionTable()) { if (functionMatcher.IsMatch(functionName) || - _fuzzyMatcher?.IsFuzzyMatch(functionName, _commandName) == true) + (_fuzzyMatcher is not null && _fuzzyMatcher.IsFuzzyMatch(functionName, _commandName))) { matchingFunction.Add(functionInfo); } @@ -1008,8 +1008,8 @@ private static bool ShouldSkipCommandResolutionForConstrainedLanguage(CommandInf { foreach (CmdletInfo cmdlet in cmdletList) { - if (cmdletMatcher?.IsMatch(cmdlet.Name) == true || - _fuzzyMatcher?.IsFuzzyMatch(cmdlet.Name, _commandName) == true) + if ((cmdletMatcher is not null && cmdletMatcher.IsMatch(cmdlet.Name)) || + (_fuzzyMatcher is not null && _fuzzyMatcher.IsFuzzyMatch(cmdlet.Name, _commandName))) { if (string.IsNullOrEmpty(moduleName) || moduleName.Equals(cmdlet.ModuleName, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 639ad15f7e3..cab29e8d13f 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -428,7 +428,7 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe foreach (KeyValuePair entry in psModule.ExportedCommands) { if (commandPattern.IsMatch(entry.Value.Name) || - fuzzyMatcher?.IsFuzzyMatch(entry.Value.Name, pattern) == true || + (fuzzyMatcher is not null && fuzzyMatcher.IsFuzzyMatch(entry.Value.Name, pattern)) || (useAbbreviationExpansion && string.Equals(pattern, AbbreviateName(entry.Value.Name), StringComparison.OrdinalIgnoreCase))) { CommandInfo current = null; @@ -488,7 +488,7 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe CommandTypes commandTypes = pair.Value; if (commandPattern.IsMatch(commandName) || - fuzzyMatcher?.IsFuzzyMatch(commandName, pattern) == true || + (fuzzyMatcher is not null && fuzzyMatcher.IsFuzzyMatch(commandName, pattern)) || (useAbbreviationExpansion && string.Equals(pattern, AbbreviateName(commandName), StringComparison.OrdinalIgnoreCase))) { bool shouldExportCommand = true; From c01acec3792ced400ed1eccce057c816e6563227 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 13 Oct 2022 12:20:50 -0700 Subject: [PATCH 3/7] More changes --- .../engine/GetCommandCommand.cs | 61 ++++++++++++++----- .../engine/Modules/ModuleUtils.cs | 21 +++++++ .../utils/FuzzyMatch.cs | 28 ++++----- 3 files changed, 80 insertions(+), 30 deletions(-) diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index eedc4ef9d73..60356b7afc6 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -346,6 +346,7 @@ public PSTypeName[] ParameterType public uint FuzzyMinimumDistance { get; set; } = 5; private FuzzyMatcher _fuzzyMatcher; + private List _commandScores = new List(); /// /// Gets or sets the parameter that determines if return cmdlets based on abbreviation expansion. @@ -508,6 +509,13 @@ private void OutputResultsHelper(IEnumerable results) { CommandOrigin origin = MyInvocation.CommandOrigin; + if (UseFuzzyMatching) + { + _commandScores = _commandScores.OrderBy(static x => x.Score).ToList(); + results = _commandScores.Select(static x => x.Command); + } + + int count = 0; foreach (CommandInfo result in results) { // Only write the command if it is visible to the requestor @@ -524,23 +532,31 @@ private void OutputResultsHelper(IEnumerable results) WriteObject(syntax); } } - else if (ShowCommandInfo.IsPresent) - { - // Write output as ShowCommandCommandInfo object. - WriteObject(ConvertToShowCommandInfo(result)); - } - else if (UseFuzzyMatching && _fuzzyMatcher.ScoreMap.TryGetValue(result.Name, out int score)) - { - // If the result was retrieved because of fuzzy searching, the attach the matching score. - var obj = new PSObject(result); - obj.Properties.Add(new PSNoteProperty("Score", score)); - WriteObject(obj); - } else { - WriteObject(result); + if (ShowCommandInfo.IsPresent) + { + // Write output as ShowCommandCommandInfo object. + WriteObject( + ConvertToShowCommandInfo(result)); + } + else + { + if (UseFuzzyMatching) + { + PSObject obj = new PSObject(result); + obj.Properties.Add(new PSNoteProperty("Score", _commandScores[count].Score)); + WriteObject(obj); + } + else + { + WriteObject(result); + } + } } } + + count += 1; } #if LEGACYTELEMETRY @@ -841,13 +857,18 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) IEnumerable commands; if (UseFuzzyMatching) { - commands = ModuleUtils.GetMatchingCommands( + foreach (var commandScore in ModuleUtils.GetFuzzyMatchingCommands( plainCommandName, Context, MyInvocation.CommandOrigin, + _fuzzyMatcher, rediscoverImportedModules: true, - moduleVersionRequired: _isFullyQualifiedModuleSpecified, - fuzzyMatcher: _fuzzyMatcher); + moduleVersionRequired: _isFullyQualifiedModuleSpecified)) + { + _commandScores.Add(commandScore); + } + + commands = _commandScores.Select(static x => x.Command); } else { @@ -1006,6 +1027,14 @@ private bool FindCommandForName(SearchResolutionOptions options, string commandN break; } + if (UseFuzzyMatching) + { + if (_fuzzyMatcher.IsFuzzyMatch(current.Name, commandName, out int score)) + { + _commandScores.Add(new CommandScore(current, score)); + } + } + _accumulatedResults.Add(current); if (ArgumentList != null) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index cab29e8d13f..a69b89744a0 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -379,6 +379,27 @@ internal static bool IsOnSystem32ModulePath(string path) #endif } + /// + /// Gets a list of fuzzy matching commands and their scores. + /// + /// Command pattern. + /// Execution context. + /// Command origin. + /// Fuzzy matcher to use. + /// If true, rediscovers imported modules. + /// Specific module version to be required. + /// IEnumerable tuple containing the CommandInfo and the match score. + internal static IEnumerable GetFuzzyMatchingCommands(string pattern, ExecutionContext context, CommandOrigin commandOrigin, FuzzyMatcher fuzzyMatcher, bool rediscoverImportedModules = false, bool moduleVersionRequired = false) + { + foreach (CommandInfo command in GetMatchingCommands(pattern, context, commandOrigin, rediscoverImportedModules, moduleVersionRequired, fuzzyMatcher: fuzzyMatcher)) + { + if (fuzzyMatcher.IsFuzzyMatch(command.Name, pattern, out int score)) + { + yield return new CommandScore(command, score); + } + } + } + /// /// Gets a list of matching commands. /// diff --git a/src/System.Management.Automation/utils/FuzzyMatch.cs b/src/System.Management.Automation/utils/FuzzyMatch.cs index c38dadc7404..bb5e2fb9474 100644 --- a/src/System.Management.Automation/utils/FuzzyMatch.cs +++ b/src/System.Management.Automation/utils/FuzzyMatch.cs @@ -8,31 +8,31 @@ namespace System.Management.Automation { internal class FuzzyMatcher { - private readonly uint _minimumDistance; - internal readonly Dictionary ScoreMap; + internal readonly uint MinimumDistance; internal FuzzyMatcher(uint minimumDistance = 5) { - _minimumDistance = minimumDistance; - ScoreMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + MinimumDistance = minimumDistance; } /// /// Determine if the two strings are considered similar. /// + internal bool IsFuzzyMatch(string candidate, string pattern) + { + return IsFuzzyMatch(candidate, pattern, out _); + } + + /// + /// Determine if the two strings are considered similar, and return the similarity score. + /// /// The candidate string to be compared. /// The pattern string to be compared with. /// True if the two strings have a distance <= MinimumDistance. - internal bool IsFuzzyMatch(string candidate, string pattern) + internal bool IsFuzzyMatch(string candidate, string pattern, out int score) { - int score = GetDamerauLevenshteinDistance(candidate, pattern); - if (score <= _minimumDistance) - { - // There could be duplicate command names during the search. - return ScoreMap.TryAdd(candidate, score); - } - - return false; + score = GetDamerauLevenshteinDistance(candidate, pattern); + return score <= MinimumDistance; } /// @@ -42,7 +42,7 @@ internal bool IsFuzzyMatch(string candidate, string pattern) /// The first string to compare. /// The second string to compare. /// The distance value where the lower the value the shorter the distance between the two strings representing a closer match. - private static int GetDamerauLevenshteinDistance(string string1, string string2) + internal static int GetDamerauLevenshteinDistance(string string1, string string2) { string1 = string1.ToUpper(CultureInfo.CurrentCulture); string2 = string2.ToUpper(CultureInfo.CurrentCulture); From fdf3373be9896e3c588022687daefdbe810c2265 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 13 Oct 2022 14:09:51 -0700 Subject: [PATCH 4/7] More fix --- src/System.Management.Automation/engine/CommandSearcher.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandSearcher.cs b/src/System.Management.Automation/engine/CommandSearcher.cs index a1e5a136134..3b1490cb3ab 100644 --- a/src/System.Management.Automation/engine/CommandSearcher.cs +++ b/src/System.Management.Automation/engine/CommandSearcher.cs @@ -1537,7 +1537,7 @@ private void setupPathSearcher() _context.CommandDiscovery.GetLookupDirectoryPaths(), _context, ConstructSearchPatternsFromName(_commandName, commandDiscovery: true), - _fuzzyMatcher); + fuzzyMatcher: null); } else if (_canDoPathLookupResult == CanDoPathLookupResult.PathIsRooted) { @@ -1561,7 +1561,7 @@ private void setupPathSearcher() directoryCollection, _context, ConstructSearchPatternsFromName(fileName, commandDiscovery: true), - _fuzzyMatcher); + fuzzyMatcher: null); } else { @@ -1601,7 +1601,7 @@ private void setupPathSearcher() directoryCollection, _context, ConstructSearchPatternsFromName(fileName, commandDiscovery: true), - _fuzzyMatcher); + fuzzyMatcher: null); } else { From fe10823921a59dc937e16d184881a1e6aba3240b Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 13 Oct 2022 14:23:20 -0700 Subject: [PATCH 5/7] One more small change --- src/System.Management.Automation/engine/GetCommandCommand.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index 60356b7afc6..b211744965a 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -346,7 +346,7 @@ public PSTypeName[] ParameterType public uint FuzzyMinimumDistance { get; set; } = 5; private FuzzyMatcher _fuzzyMatcher; - private List _commandScores = new List(); + private List _commandScores; /// /// Gets or sets the parameter that determines if return cmdlets based on abbreviation expansion. @@ -371,6 +371,7 @@ protected override void BeginProcessing() if (UseFuzzyMatching) { _fuzzyMatcher = new FuzzyMatcher(FuzzyMinimumDistance); + _commandScores = new List(); } if (ShowCommandInfo.IsPresent && Syntax.IsPresent) From aebeed4123ca2d2940c9a1e5c869798a2b9b96c1 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 17 Oct 2022 14:37:51 -0700 Subject: [PATCH 6/7] Address feedback --- src/System.Management.Automation/engine/GetCommandCommand.cs | 2 +- src/System.Management.Automation/utils/FuzzyMatch.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index b211744965a..bb06d58a75a 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -343,7 +343,7 @@ public PSTypeName[] ParameterType /// Gets or sets the minimum fuzzy matching distance. /// [Parameter(ParameterSetName = "AllCommandSet")] - public uint FuzzyMinimumDistance { get; set; } = 5; + public uint FuzzyMinimumDistance { get; set; } = 3; private FuzzyMatcher _fuzzyMatcher; private List _commandScores; diff --git a/src/System.Management.Automation/utils/FuzzyMatch.cs b/src/System.Management.Automation/utils/FuzzyMatch.cs index bb5e2fb9474..c4e542a3ca5 100644 --- a/src/System.Management.Automation/utils/FuzzyMatch.cs +++ b/src/System.Management.Automation/utils/FuzzyMatch.cs @@ -10,7 +10,7 @@ internal class FuzzyMatcher { internal readonly uint MinimumDistance; - internal FuzzyMatcher(uint minimumDistance = 5) + internal FuzzyMatcher(uint minimumDistance) { MinimumDistance = minimumDistance; } From 14d125328e0f813d50394933775ef4e440dc5727 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 17 Oct 2022 15:58:32 -0700 Subject: [PATCH 7/7] Revert back to use 5 as the default min distance --- src/System.Management.Automation/engine/GetCommandCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index bb06d58a75a..b211744965a 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -343,7 +343,7 @@ public PSTypeName[] ParameterType /// Gets or sets the minimum fuzzy matching distance. /// [Parameter(ParameterSetName = "AllCommandSet")] - public uint FuzzyMinimumDistance { get; set; } = 3; + public uint FuzzyMinimumDistance { get; set; } = 5; private FuzzyMatcher _fuzzyMatcher; private List _commandScores;