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..3b1490cb3ab 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 is not null && _fuzzyMatcher.IsFuzzyMatch(aliasEntry.Key, _commandName))) { 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 is not null && _fuzzyMatcher.IsFuzzyMatch(functionName, _commandName))) { 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 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)) { @@ -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: null); } else if (_canDoPathLookupResult == CanDoPathLookupResult.PathIsRooted) { @@ -1568,7 +1561,7 @@ private void setupPathSearcher() directoryCollection, _context, ConstructSearchPatternsFromName(fileName, commandDiscovery: true), - useFuzzyMatch: false); + fuzzyMatcher: null); } else { @@ -1608,7 +1601,7 @@ private void setupPathSearcher() directoryCollection, _context, ConstructSearchPatternsFromName(fileName, commandDiscovery: true), - useFuzzyMatch: false); + fuzzyMatcher: null); } 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..b211744965a 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -345,7 +345,8 @@ public PSTypeName[] ParameterType [Parameter(ParameterSetName = "AllCommandSet")] public uint FuzzyMinimumDistance { get; set; } = 5; - private List _commandScores = new List(); + private FuzzyMatcher _fuzzyMatcher; + private List _commandScores; /// /// Gets or sets the parameter that determines if return cmdlets based on abbreviation expansion. @@ -367,7 +368,11 @@ protected override void BeginProcessing() #if LEGACYTELEMETRY _timer.Start(); #endif - base.BeginProcessing(); + if (UseFuzzyMatching) + { + _fuzzyMatcher = new FuzzyMatcher(FuzzyMinimumDistance); + _commandScores = new List(); + } if (ShowCommandInfo.IsPresent && Syntax.IsPresent) { @@ -503,14 +508,11 @@ protected override void EndProcessing() private void OutputResultsHelper(IEnumerable results) { - CommandOrigin origin = this.MyInvocation.CommandOrigin; + CommandOrigin origin = MyInvocation.CommandOrigin; if (UseFuzzyMatching) { - _commandScores = _commandScores - .Where(x => x.Score <= FuzzyMinimumDistance) - .OrderBy(static x => x.Score) - .ToList(); + _commandScores = _commandScores.OrderBy(static x => x.Score).ToList(); results = _commandScores.Select(static x => x.Command); } @@ -784,11 +786,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 +858,25 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) IEnumerable commands; if (UseFuzzyMatching) { - foreach (var commandScore in System.Management.Automation.Internal.ModuleUtils.GetFuzzyMatchingCommands( + foreach (var commandScore in ModuleUtils.GetFuzzyMatchingCommands( plainCommandName, - this.Context, - this.MyInvocation.CommandOrigin, + Context, + MyInvocation.CommandOrigin, + _fuzzyMatcher, rediscoverImportedModules: true, moduleVersionRequired: _isFullyQualifiedModuleSpecified)) { _commandScores.Add(commandScore); } - commands = _commandScores.Select(static x => x.Command).ToList(); + commands = _commandScores.Select(static x => x.Command); } 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 +937,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; @@ -1032,8 +1030,10 @@ private bool FindCommandForName(SearchResolutionOptions options, string commandN if (UseFuzzyMatching) { - int score = FuzzyMatcher.GetDamerauLevenshteinDistance(current.Name, commandName); - _commandScores.Add(new CommandScore(current, score)); + if (_fuzzyMatcher.IsFuzzyMatch(current.Name, commandName, out int score)) + { + _commandScores.Add(new CommandScore(current, score)); + } } _accumulatedResults.Add(current); diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 8dab2283fc5..a69b89744a0 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -385,15 +385,15 @@ internal static bool IsOnSystem32ModulePath(string path) /// 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, bool rediscoverImportedModules = false, bool moduleVersionRequired = false) + 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, useFuzzyMatching: true)) + foreach (CommandInfo command in GetMatchingCommands(pattern, context, commandOrigin, rediscoverImportedModules, moduleVersionRequired, fuzzyMatcher: fuzzyMatcher)) { - int score = FuzzyMatcher.GetDamerauLevenshteinDistance(command.Name, pattern); - if (score <= FuzzyMatcher.MinimumDistance) + if (fuzzyMatcher.IsFuzzyMatch(command.Name, pattern, out int score)) { yield return new CommandScore(command, score); } @@ -408,10 +408,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 +449,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 is not null && fuzzyMatcher.IsFuzzyMatch(entry.Value.Name, pattern)) || (useAbbreviationExpansion && string.Equals(pattern, AbbreviateName(entry.Value.Name), StringComparison.OrdinalIgnoreCase))) { CommandInfo current = null; @@ -509,7 +509,7 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe CommandTypes commandTypes = pair.Value; if (commandPattern.IsMatch(commandName) || - (useFuzzyMatching && FuzzyMatcher.IsFuzzyMatch(commandName, pattern)) || + (fuzzyMatcher is not null && fuzzyMatcher.IsFuzzyMatch(commandName, pattern)) || (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..c4e542a3ca5 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; + internal readonly uint MinimumDistance; + + internal FuzzyMatcher(uint minimumDistance) + { + MinimumDistance = minimumDistance; + } /// /// Determine if the two strings are considered similar. /// - /// The first string to compare. - /// The second string to compare. + 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. - public static bool IsFuzzyMatch(string string1, string string2) + internal bool IsFuzzyMatch(string candidate, string pattern, out int score) { - return GetDamerauLevenshteinDistance(string1, string2) <= MinimumDistance; + score = GetDamerauLevenshteinDistance(candidate, pattern); + return score <= MinimumDistance; } /// @@ -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) + internal 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++) {