Make the fuzzy searching flexible by passing in the fuzzy matcher (#18270)

This commit is contained in:
Dongbo Wang
2022-10-17 16:40:54 -07:00
committed by GitHub
parent 0b8c3de40a
commit bc1f3691f2
5 changed files with 100 additions and 88 deletions
@@ -37,17 +37,17 @@ namespace System.Management.Automation
/// <param name="acceptableCommandNames">
/// The patterns to search for in the paths.
/// </param>
/// <param name="useFuzzyMatch">
/// Use likely relevant search.
/// <param name="fuzzyMatcher">
/// The fuzzy matcher to use for fuzzy searching.
/// </param>
internal CommandPathSearch(
string commandName,
LookupPathCollection lookupPaths,
ExecutionContext context,
Collection<string>? acceptableCommandNames,
bool useFuzzyMatch)
FuzzyMatcher? fuzzyMatcher)
{
_useFuzzyMatch = useFuzzyMatch;
_fuzzyMatcher = fuzzyMatcher;
string[] commandPatterns;
if (acceptableCommandNames != null)
{
@@ -434,13 +434,13 @@ namespace System.Management.Automation
// to forcefully use null if pattern is "."
if (pattern.Length != 1 || pattern[0] != '.')
{
if (_useFuzzyMatch)
if (_fuzzyMatcher is not null)
{
var files = new List<string>();
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 @@ namespace System.Management.Automation
private readonly string[] _orderedPathExt;
private readonly Collection<string>? _acceptableCommandNames;
private readonly bool _useFuzzyMatch = false;
private readonly FuzzyMatcher? _fuzzyMatcher;
#endregion private members
}
@@ -24,29 +24,20 @@ namespace System.Management.Automation
/// Constructs a command searching enumerator that resolves the location
/// to a command using a standard algorithm.
/// </summary>
/// <param name="commandName">
/// The name of the command to look for.
/// </param>
/// <param name="options">
/// Determines which types of commands glob resolution of the name will take place on.
/// </param>
/// <param name="commandTypes">
/// The types of commands to look for.
/// </param>
/// <param name="context">
/// The execution context for this engine instance...
/// </param>
/// <exception cref="ArgumentNullException">
/// If <paramref name="context"/> is null.
/// </exception>
/// <exception cref="PSArgumentException">
/// If <paramref name="commandName"/> is null or empty.
/// </exception>
/// <param name="commandName">The name of the command to look for.</param>
/// <param name="options">Determines which types of commands glob resolution of the name will take place on.</param>
/// <param name="commandTypes">The types of commands to look for.</param>
/// <param name="context">The execution context for this engine instance.</param>
/// <param name="fuzzyMatcher">The fuzzy matcher to use for fuzzy searching.</param>
///
/// <exception cref="ArgumentNullException">If <paramref name="context"/> is null.</exception>
/// <exception cref="PSArgumentException">If <paramref name="commandName"/> is null or empty.</exception>
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 @@ namespace System.Management.Automation
_context = context;
_commandResolutionOptions = options;
_commandTypes = commandTypes;
_fuzzyMatcher = fuzzyMatcher;
// Initialize the enumerators
this.Reset();
@@ -705,8 +697,7 @@ namespace System.Management.Automation
foreach (KeyValuePair<string, AliasInfo> 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 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
{
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 @@ namespace System.Management.Automation
/// </summary>
private readonly ExecutionContext _context;
/// <summary>
/// The fuzzy matcher to use for fuzzy searching.
/// </summary>
private readonly FuzzyMatcher? _fuzzyMatcher;
/// <summary>
/// A routine to initialize the path searcher...
/// </summary>
@@ -1528,7 +1521,7 @@ namespace System.Management.Automation
_context.CommandDiscovery.GetLookupDirectoryPaths(),
_context,
acceptableCommandNames: null,
useFuzzyMatch: _commandResolutionOptions.HasFlag(SearchResolutionOptions.FuzzyMatch));
_fuzzyMatcher);
}
else
{
@@ -1544,7 +1537,7 @@ namespace System.Management.Automation
_context.CommandDiscovery.GetLookupDirectoryPaths(),
_context,
ConstructSearchPatternsFromName(_commandName, commandDiscovery: true),
useFuzzyMatch: false);
fuzzyMatcher: null);
}
else if (_canDoPathLookupResult == CanDoPathLookupResult.PathIsRooted)
{
@@ -1568,7 +1561,7 @@ namespace System.Management.Automation
directoryCollection,
_context,
ConstructSearchPatternsFromName(fileName, commandDiscovery: true),
useFuzzyMatch: false);
fuzzyMatcher: null);
}
else
{
@@ -1608,7 +1601,7 @@ namespace System.Management.Automation
directoryCollection,
_context,
ConstructSearchPatternsFromName(fileName, commandDiscovery: true),
useFuzzyMatch: false);
fuzzyMatcher: null);
}
else
{
@@ -1727,17 +1720,14 @@ namespace System.Management.Automation
CommandNameIsPattern = 0x04,
SearchAllScopes = 0x08,
/// <summary>Use fuzzy matching.</summary>
FuzzyMatch = 0x10,
/// <summary>
/// Enable searching for cmdlets/functions by abbreviation expansion.
/// </summary>
UseAbbreviationExpansion = 0x20,
UseAbbreviationExpansion = 0x10,
/// <summary>
/// Enable resolving wildcard in paths.
/// </summary>
ResolveLiteralThenPathPatterns = 0x40
ResolveLiteralThenPathPatterns = 0x20
}
}
@@ -345,7 +345,8 @@ namespace Microsoft.PowerShell.Commands
[Parameter(ParameterSetName = "AllCommandSet")]
public uint FuzzyMinimumDistance { get; set; } = 5;
private List<CommandScore> _commandScores = new List<CommandScore>();
private FuzzyMatcher _fuzzyMatcher;
private List<CommandScore> _commandScores;
/// <summary>
/// Gets or sets the parameter that determines if return cmdlets based on abbreviation expansion.
@@ -367,7 +368,11 @@ namespace Microsoft.PowerShell.Commands
#if LEGACYTELEMETRY
_timer.Start();
#endif
base.BeginProcessing();
if (UseFuzzyMatching)
{
_fuzzyMatcher = new FuzzyMatcher(FuzzyMinimumDistance);
_commandScores = new List<CommandScore>();
}
if (ShowCommandInfo.IsPresent && Syntax.IsPresent)
{
@@ -503,14 +508,11 @@ namespace Microsoft.PowerShell.Commands
private void OutputResultsHelper(IEnumerable<CommandInfo> 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 @@ namespace Microsoft.PowerShell.Commands
options |= SearchResolutionOptions.UseAbbreviationExpansion;
}
if (UseFuzzyMatching)
{
options |= SearchResolutionOptions.FuzzyMatch;
}
if ((this.CommandType & CommandTypes.Alias) != 0)
{
options |= SearchResolutionOptions.ResolveAliasPatterns;
@@ -861,24 +858,25 @@ namespace Microsoft.PowerShell.Commands
IEnumerable<CommandInfo> 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 @@ namespace Microsoft.PowerShell.Commands
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 @@ namespace Microsoft.PowerShell.Commands
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);
@@ -385,15 +385,15 @@ namespace System.Management.Automation.Internal
/// <param name="pattern">Command pattern.</param>
/// <param name="context">Execution context.</param>
/// <param name="commandOrigin">Command origin.</param>
/// <param name="fuzzyMatcher">Fuzzy matcher to use.</param>
/// <param name="rediscoverImportedModules">If true, rediscovers imported modules.</param>
/// <param name="moduleVersionRequired">Specific module version to be required.</param>
/// <returns>IEnumerable tuple containing the CommandInfo and the match score.</returns>
internal static IEnumerable<CommandScore> GetFuzzyMatchingCommands(string pattern, ExecutionContext context, CommandOrigin commandOrigin, bool rediscoverImportedModules = false, bool moduleVersionRequired = false)
internal static IEnumerable<CommandScore> 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 @@ namespace System.Management.Automation.Internal
/// <param name="commandOrigin">Command origin.</param>
/// <param name="rediscoverImportedModules">If true, rediscovers imported modules.</param>
/// <param name="moduleVersionRequired">Specific module version to be required.</param>
/// <param name="useFuzzyMatching">Use fuzzy matching.</param>
/// <param name="fuzzyMatcher">Fuzzy matcher for fuzzy searching.</param>
/// <param name="useAbbreviationExpansion">Use abbreviation expansion for matching.</param>
/// <returns>Returns matching CommandInfo IEnumerable.</returns>
internal static IEnumerable<CommandInfo> GetMatchingCommands(string pattern, ExecutionContext context, CommandOrigin commandOrigin, bool rediscoverImportedModules = false, bool moduleVersionRequired = false, bool useFuzzyMatching = false, bool useAbbreviationExpansion = false)
internal static IEnumerable<CommandInfo> 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 @@ namespace System.Management.Automation.Internal
foreach (KeyValuePair<string, CommandInfo> 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 @@ namespace System.Management.Automation.Internal
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;
@@ -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;
}
/// <summary>
/// Determine if the two strings are considered similar.
/// </summary>
/// <param name="string1">The first string to compare.</param>
/// <param name="string2">The second string to compare.</param>
/// <returns>True if the two strings have a distance <= MinimumDistance.</returns>
public static bool IsFuzzyMatch(string string1, string string2)
internal bool IsFuzzyMatch(string candidate, string pattern)
{
return GetDamerauLevenshteinDistance(string1, string2) <= MinimumDistance;
return IsFuzzyMatch(candidate, pattern, out _);
}
/// <summary>
/// Determine if the two strings are considered similar, and return the similarity score.
/// </summary>
/// <param name="candidate">The candidate string to be compared.</param>
/// <param name="pattern">The pattern string to be compared with.</param>
/// <returns>True if the two strings have a distance <= MinimumDistance.</returns>
internal bool IsFuzzyMatch(string candidate, string pattern, out int score)
{
score = GetDamerauLevenshteinDistance(candidate, pattern);
return score <= MinimumDistance;
}
/// <summary>
@@ -27,7 +42,7 @@ namespace System.Management.Automation
/// <param name="string1">The first string to compare.</param>
/// <param name="string2">The second string to compare.</param>
/// <returns>The distance value where the lower the value the shorter the distance between the two strings representing a closer match.</returns>
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 @@ namespace System.Management.Automation
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++)
{