mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Make the fuzzy searching flexible by passing in the fuzzy matcher (#18270)
This commit is contained in:
@@ -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++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user