Skip to content

Commit bc1f369

Browse files
authored
Make the fuzzy searching flexible by passing in the fuzzy matcher (PowerShell#18270)
1 parent 0b8c3de commit bc1f369

File tree

5 files changed

+99
-87
lines changed

5 files changed

+99
-87
lines changed

src/System.Management.Automation/engine/CommandPathSearch.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,17 @@ internal class CommandPathSearch : IEnumerable<string>, IEnumerator<string>
3737
/// <param name="acceptableCommandNames">
3838
/// The patterns to search for in the paths.
3939
/// </param>
40-
/// <param name="useFuzzyMatch">
41-
/// Use likely relevant search.
40+
/// <param name="fuzzyMatcher">
41+
/// The fuzzy matcher to use for fuzzy searching.
4242
/// </param>
4343
internal CommandPathSearch(
4444
string commandName,
4545
LookupPathCollection lookupPaths,
4646
ExecutionContext context,
4747
Collection<string>? acceptableCommandNames,
48-
bool useFuzzyMatch)
48+
FuzzyMatcher? fuzzyMatcher)
4949
{
50-
_useFuzzyMatch = useFuzzyMatch;
50+
_fuzzyMatcher = fuzzyMatcher;
5151
string[] commandPatterns;
5252
if (acceptableCommandNames != null)
5353
{
@@ -434,13 +434,13 @@ private void GetNewDirectoryResults(string pattern, string directory)
434434
// to forcefully use null if pattern is "."
435435
if (pattern.Length != 1 || pattern[0] != '.')
436436
{
437-
if (_useFuzzyMatch)
437+
if (_fuzzyMatcher is not null)
438438
{
439439
var files = new List<string>();
440440
var matchingFiles = Directory.EnumerateFiles(directory);
441441
foreach (string file in matchingFiles)
442442
{
443-
if (FuzzyMatcher.IsFuzzyMatch(Path.GetFileName(file), pattern))
443+
if (_fuzzyMatcher.IsFuzzyMatch(Path.GetFileName(file), pattern))
444444
{
445445
files.Add(file);
446446
}
@@ -589,7 +589,7 @@ private void GetNewDirectoryResults(string pattern, string directory)
589589
private readonly string[] _orderedPathExt;
590590
private readonly Collection<string>? _acceptableCommandNames;
591591

592-
private readonly bool _useFuzzyMatch = false;
592+
private readonly FuzzyMatcher? _fuzzyMatcher;
593593

594594
#endregion private members
595595
}

src/System.Management.Automation/engine/CommandSearcher.cs

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,20 @@ internal class CommandSearcher : IEnumerable<CommandInfo>, IEnumerator<CommandIn
2424
/// Constructs a command searching enumerator that resolves the location
2525
/// to a command using a standard algorithm.
2626
/// </summary>
27-
/// <param name="commandName">
28-
/// The name of the command to look for.
29-
/// </param>
30-
/// <param name="options">
31-
/// Determines which types of commands glob resolution of the name will take place on.
32-
/// </param>
33-
/// <param name="commandTypes">
34-
/// The types of commands to look for.
35-
/// </param>
36-
/// <param name="context">
37-
/// The execution context for this engine instance...
38-
/// </param>
39-
/// <exception cref="ArgumentNullException">
40-
/// If <paramref name="context"/> is null.
41-
/// </exception>
42-
/// <exception cref="PSArgumentException">
43-
/// If <paramref name="commandName"/> is null or empty.
44-
/// </exception>
27+
/// <param name="commandName">The name of the command to look for.</param>
28+
/// <param name="options">Determines which types of commands glob resolution of the name will take place on.</param>
29+
/// <param name="commandTypes">The types of commands to look for.</param>
30+
/// <param name="context">The execution context for this engine instance.</param>
31+
/// <param name="fuzzyMatcher">The fuzzy matcher to use for fuzzy searching.</param>
32+
///
33+
/// <exception cref="ArgumentNullException">If <paramref name="context"/> is null.</exception>
34+
/// <exception cref="PSArgumentException">If <paramref name="commandName"/> is null or empty.</exception>
4535
internal CommandSearcher(
4636
string commandName,
4737
SearchResolutionOptions options,
4838
CommandTypes commandTypes,
49-
ExecutionContext context)
39+
ExecutionContext context,
40+
FuzzyMatcher? fuzzyMatcher = null)
5041
{
5142
Diagnostics.Assert(context != null, "caller to verify context is not null");
5243
Diagnostics.Assert(!string.IsNullOrEmpty(commandName), "caller to verify commandName is valid");
@@ -55,6 +46,7 @@ internal CommandSearcher(
5546
_context = context;
5647
_commandResolutionOptions = options;
5748
_commandTypes = commandTypes;
49+
_fuzzyMatcher = fuzzyMatcher;
5850

5951
// Initialize the enumerators
6052
this.Reset();
@@ -705,8 +697,7 @@ private static bool checkPath(string path, string commandName)
705697
foreach (KeyValuePair<string, AliasInfo> aliasEntry in _context.EngineSessionState.GetAliasTable())
706698
{
707699
if (aliasMatcher.IsMatch(aliasEntry.Key) ||
708-
(_commandResolutionOptions.HasFlag(SearchResolutionOptions.FuzzyMatch) &&
709-
FuzzyMatcher.IsFuzzyMatch(aliasEntry.Key, _commandName)))
700+
(_fuzzyMatcher is not null && _fuzzyMatcher.IsFuzzyMatch(aliasEntry.Key, _commandName)))
710701
{
711702
matchingAliases.Add(aliasEntry.Value);
712703
}
@@ -785,8 +776,7 @@ private static bool checkPath(string path, string commandName)
785776
foreach ((string functionName, FunctionInfo functionInfo) in _context.EngineSessionState.GetFunctionTable())
786777
{
787778
if (functionMatcher.IsMatch(functionName) ||
788-
(_commandResolutionOptions.HasFlag(SearchResolutionOptions.FuzzyMatch) &&
789-
FuzzyMatcher.IsFuzzyMatch(functionName, _commandName)))
779+
(_fuzzyMatcher is not null && _fuzzyMatcher.IsFuzzyMatch(functionName, _commandName)))
790780
{
791781
matchingFunction.Add(functionInfo);
792782
}
@@ -1018,10 +1008,8 @@ private static bool ShouldSkipCommandResolutionForConstrainedLanguage(CommandInf
10181008
{
10191009
foreach (CmdletInfo cmdlet in cmdletList)
10201010
{
1021-
if (cmdletMatcher != null &&
1022-
cmdletMatcher.IsMatch(cmdlet.Name) ||
1023-
(_commandResolutionOptions.HasFlag(SearchResolutionOptions.FuzzyMatch) &&
1024-
FuzzyMatcher.IsFuzzyMatch(cmdlet.Name, _commandName)))
1011+
if ((cmdletMatcher is not null && cmdletMatcher.IsMatch(cmdlet.Name)) ||
1012+
(_fuzzyMatcher is not null && _fuzzyMatcher.IsFuzzyMatch(cmdlet.Name, _commandName)))
10251013
{
10261014
if (string.IsNullOrEmpty(moduleName) || moduleName.Equals(cmdlet.ModuleName, StringComparison.OrdinalIgnoreCase))
10271015
{
@@ -1496,6 +1484,11 @@ private static CanDoPathLookupResult CanDoPathLookup(string possiblePath)
14961484
/// </summary>
14971485
private readonly ExecutionContext _context;
14981486

1487+
/// <summary>
1488+
/// The fuzzy matcher to use for fuzzy searching.
1489+
/// </summary>
1490+
private readonly FuzzyMatcher? _fuzzyMatcher;
1491+
14991492
/// <summary>
15001493
/// A routine to initialize the path searcher...
15011494
/// </summary>
@@ -1528,7 +1521,7 @@ private void setupPathSearcher()
15281521
_context.CommandDiscovery.GetLookupDirectoryPaths(),
15291522
_context,
15301523
acceptableCommandNames: null,
1531-
useFuzzyMatch: _commandResolutionOptions.HasFlag(SearchResolutionOptions.FuzzyMatch));
1524+
_fuzzyMatcher);
15321525
}
15331526
else
15341527
{
@@ -1544,7 +1537,7 @@ private void setupPathSearcher()
15441537
_context.CommandDiscovery.GetLookupDirectoryPaths(),
15451538
_context,
15461539
ConstructSearchPatternsFromName(_commandName, commandDiscovery: true),
1547-
useFuzzyMatch: false);
1540+
fuzzyMatcher: null);
15481541
}
15491542
else if (_canDoPathLookupResult == CanDoPathLookupResult.PathIsRooted)
15501543
{
@@ -1568,7 +1561,7 @@ private void setupPathSearcher()
15681561
directoryCollection,
15691562
_context,
15701563
ConstructSearchPatternsFromName(fileName, commandDiscovery: true),
1571-
useFuzzyMatch: false);
1564+
fuzzyMatcher: null);
15721565
}
15731566
else
15741567
{
@@ -1608,7 +1601,7 @@ private void setupPathSearcher()
16081601
directoryCollection,
16091602
_context,
16101603
ConstructSearchPatternsFromName(fileName, commandDiscovery: true),
1611-
useFuzzyMatch: false);
1604+
fuzzyMatcher: null);
16121605
}
16131606
else
16141607
{
@@ -1727,17 +1720,14 @@ internal enum SearchResolutionOptions
17271720
CommandNameIsPattern = 0x04,
17281721
SearchAllScopes = 0x08,
17291722

1730-
/// <summary>Use fuzzy matching.</summary>
1731-
FuzzyMatch = 0x10,
1732-
17331723
/// <summary>
17341724
/// Enable searching for cmdlets/functions by abbreviation expansion.
17351725
/// </summary>
1736-
UseAbbreviationExpansion = 0x20,
1726+
UseAbbreviationExpansion = 0x10,
17371727

17381728
/// <summary>
17391729
/// Enable resolving wildcard in paths.
17401730
/// </summary>
1741-
ResolveLiteralThenPathPatterns = 0x40
1731+
ResolveLiteralThenPathPatterns = 0x20
17421732
}
17431733
}

src/System.Management.Automation/engine/GetCommandCommand.cs

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,8 @@ public PSTypeName[] ParameterType
345345
[Parameter(ParameterSetName = "AllCommandSet")]
346346
public uint FuzzyMinimumDistance { get; set; } = 5;
347347

348-
private List<CommandScore> _commandScores = new List<CommandScore>();
348+
private FuzzyMatcher _fuzzyMatcher;
349+
private List<CommandScore> _commandScores;
349350

350351
/// <summary>
351352
/// Gets or sets the parameter that determines if return cmdlets based on abbreviation expansion.
@@ -367,7 +368,11 @@ protected override void BeginProcessing()
367368
#if LEGACYTELEMETRY
368369
_timer.Start();
369370
#endif
370-
base.BeginProcessing();
371+
if (UseFuzzyMatching)
372+
{
373+
_fuzzyMatcher = new FuzzyMatcher(FuzzyMinimumDistance);
374+
_commandScores = new List<CommandScore>();
375+
}
371376

372377
if (ShowCommandInfo.IsPresent && Syntax.IsPresent)
373378
{
@@ -503,14 +508,11 @@ protected override void EndProcessing()
503508

504509
private void OutputResultsHelper(IEnumerable<CommandInfo> results)
505510
{
506-
CommandOrigin origin = this.MyInvocation.CommandOrigin;
511+
CommandOrigin origin = MyInvocation.CommandOrigin;
507512

508513
if (UseFuzzyMatching)
509514
{
510-
_commandScores = _commandScores
511-
.Where(x => x.Score <= FuzzyMinimumDistance)
512-
.OrderBy(static x => x.Score)
513-
.ToList();
515+
_commandScores = _commandScores.OrderBy(static x => x.Score).ToList();
514516
results = _commandScores.Select(static x => x.Command);
515517
}
516518

@@ -784,11 +786,6 @@ private void AccumulateMatchingCommands(IEnumerable<string> commandNames)
784786
options |= SearchResolutionOptions.UseAbbreviationExpansion;
785787
}
786788

787-
if (UseFuzzyMatching)
788-
{
789-
options |= SearchResolutionOptions.FuzzyMatch;
790-
}
791-
792789
if ((this.CommandType & CommandTypes.Alias) != 0)
793790
{
794791
options |= SearchResolutionOptions.ResolveAliasPatterns;
@@ -861,24 +858,25 @@ private void AccumulateMatchingCommands(IEnumerable<string> commandNames)
861858
IEnumerable<CommandInfo> commands;
862859
if (UseFuzzyMatching)
863860
{
864-
foreach (var commandScore in System.Management.Automation.Internal.ModuleUtils.GetFuzzyMatchingCommands(
861+
foreach (var commandScore in ModuleUtils.GetFuzzyMatchingCommands(
865862
plainCommandName,
866-
this.Context,
867-
this.MyInvocation.CommandOrigin,
863+
Context,
864+
MyInvocation.CommandOrigin,
865+
_fuzzyMatcher,
868866
rediscoverImportedModules: true,
869867
moduleVersionRequired: _isFullyQualifiedModuleSpecified))
870868
{
871869
_commandScores.Add(commandScore);
872870
}
873871

874-
commands = _commandScores.Select(static x => x.Command).ToList();
872+
commands = _commandScores.Select(static x => x.Command);
875873
}
876874
else
877875
{
878-
commands = System.Management.Automation.Internal.ModuleUtils.GetMatchingCommands(
876+
commands = ModuleUtils.GetMatchingCommands(
879877
plainCommandName,
880-
this.Context,
881-
this.MyInvocation.CommandOrigin,
878+
Context,
879+
MyInvocation.CommandOrigin,
882880
rediscoverImportedModules: true,
883881
moduleVersionRequired: _isFullyQualifiedModuleSpecified,
884882
useAbbreviationExpansion: UseAbbreviationExpansion);
@@ -939,12 +937,12 @@ private void AccumulateMatchingCommands(IEnumerable<string> commandNames)
939937

940938
private bool FindCommandForName(SearchResolutionOptions options, string commandName, bool isPattern, bool emitErrors, ref int currentCount, out bool isDuplicate)
941939
{
942-
CommandSearcher searcher =
943-
new CommandSearcher(
944-
commandName,
945-
options,
946-
this.CommandType,
947-
this.Context);
940+
var searcher = new CommandSearcher(
941+
commandName,
942+
options,
943+
CommandType,
944+
Context,
945+
_fuzzyMatcher);
948946

949947
bool resultFound = false;
950948
isDuplicate = false;
@@ -1032,8 +1030,10 @@ private bool FindCommandForName(SearchResolutionOptions options, string commandN
10321030

10331031
if (UseFuzzyMatching)
10341032
{
1035-
int score = FuzzyMatcher.GetDamerauLevenshteinDistance(current.Name, commandName);
1036-
_commandScores.Add(new CommandScore(current, score));
1033+
if (_fuzzyMatcher.IsFuzzyMatch(current.Name, commandName, out int score))
1034+
{
1035+
_commandScores.Add(new CommandScore(current, score));
1036+
}
10371037
}
10381038

10391039
_accumulatedResults.Add(current);

0 commit comments

Comments
 (0)