From 1f54e88b771ae6a2665050e2b1564b5f39d3554f Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 23 Oct 2018 13:46:00 -0700 Subject: [PATCH 1/7] [feature] enable experimental feature to support tab completion on abbreviations of cmdlets --- .../CommandCompletion/CompletionCompleters.cs | 103 +++++++++++------- .../engine/CommandDiscovery.cs | 44 +++++++- .../engine/CommandSearcher.cs | 38 +++++-- .../ExperimentalFeature.cs | 6 +- .../engine/GetCommandCommand.cs | 17 ++- .../TabCompletion/TabCompletion.Tests.ps1 | 29 +++++ .../Get-Command.Tests.ps1 | 66 +++++++++++ 7 files changed, 249 insertions(+), 54 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 6f0715ef449..d4bfe71b682 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -87,7 +87,6 @@ private static List CompleteCommand(CompletionContext context, string commandName = context.WordToComplete; string quote = HandleDoubleAndSingleQuote(ref commandName); - commandName += "*"; List commandResults = null; if (commandName.IndexOfAny(Utils.Separators.DirectoryOrDrive) == -1) @@ -100,29 +99,7 @@ private static List CompleteCommand(CompletionContext context, lastAst = context.RelatedAsts.Last(); } - var powershell = context.Helper - .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) - .AddParameter("All") - .AddParameter("Name", commandName); - - if (moduleName != null) - powershell.AddParameter("Module", moduleName); - if (!types.Equals(CommandTypes.All)) - powershell.AddParameter("CommandType", types); - - Exception exceptionThrown; - var commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); - - if (commandInfos != null && commandInfos.Count > 1) - { - // OrderBy is using stable sorting - var sortedCommandInfos = commandInfos.OrderBy(a => a, new CommandNameComparer()); - commandResults = MakeCommandsUnique(sortedCommandInfos, /* includeModulePrefix: */ false, addAmpersandIfNecessary, quote); - } - else - { - commandResults = MakeCommandsUnique(commandInfos, /* includeModulePrefix: */ false, addAmpersandIfNecessary, quote); - } + commandResults = ExecuteGetCommandCommand(context, moduleName, types, includeModulePrefix: false); if (lastAst != null) { @@ -159,28 +136,74 @@ private static List CompleteCommand(CompletionContext context, moduleName = commandName.Substring(0, indexOfFirstBackslash); commandName = commandName.Substring(indexOfFirstBackslash + 1); - var powershell = context.Helper - .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) - .AddParameter("All") - .AddParameter("Name", commandName) - .AddParameter("Module", moduleName); + commandResults = ExecuteGetCommandCommand(context, moduleName, types, includeModulePrefix: true); + } + } - if (!types.Equals(CommandTypes.All)) - powershell.AddParameter("CommandType", types); + return commandResults; + } - Exception exceptionThrown; - var commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); + private static List ExecuteGetCommandCommand(CompletionContext context, string moduleName, CommandTypes types, bool includeModulePrefix) + { + var addAmpersandIfNecessary = IsAmpersandNeeded(context, false); + string commandName = context.WordToComplete; + string quote = HandleDoubleAndSingleQuote(ref commandName); - if (commandInfos != null && commandInfos.Count > 1) - { - var sortedCommandInfos = commandInfos.OrderBy(a => a, new CommandNameComparer()); - commandResults = MakeCommandsUnique(sortedCommandInfos, /* includeModulePrefix: */ true, addAmpersandIfNecessary, quote); - } - else + var powershell = context.Helper + .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) + .AddParameter("All") + .AddParameter("Name", commandName + "*"); + + if (moduleName != null) + { + powershell.AddParameter("Module", moduleName); + } + + if (!types.Equals(CommandTypes.All)) + { + powershell.AddParameter("CommandType", types); + } + + Exception exceptionThrown; + var commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); + + if (commandInfos == null || commandInfos.Count == 0) + { + powershell.Commands.Clear(); + powershell + .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) + .AddParameter("All") + .AddParameter("Name", commandName); + + if (ExperimentalFeature.IsEnabled("PSUseAbbreviationExpansion")) { - commandResults = MakeCommandsUnique(commandInfos, /* includeModulePrefix: */ true, addAmpersandIfNecessary, quote); + powershell.AddParameter("UseAbbreviationExpansion"); } + + if (moduleName != null) + { + powershell.AddParameter("Module", moduleName); } + + if (!types.Equals(CommandTypes.All)) + { + powershell.AddParameter("CommandType", types); + } + + commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); + } + + List commandResults = null; + + if (commandInfos != null && commandInfos.Count > 1) + { + // OrderBy is using stable sorting + var sortedCommandInfos = commandInfos.OrderBy(a => a, new CommandNameComparer()); + commandResults = MakeCommandsUnique(sortedCommandInfos, includeModulePrefix, addAmpersandIfNecessary, quote); + } + else + { + commandResults = MakeCommandsUnique(commandInfos, includeModulePrefix, addAmpersandIfNecessary, quote); } return commandResults; diff --git a/src/System.Management.Automation/engine/CommandDiscovery.cs b/src/System.Management.Automation/engine/CommandDiscovery.cs index 2d2af3fa43b..92c4d1f5a19 100644 --- a/src/System.Management.Automation/engine/CommandDiscovery.cs +++ b/src/System.Management.Automation/engine/CommandDiscovery.cs @@ -1131,11 +1131,27 @@ private static CommandInfo TryModuleAutoDiscovery(string commandName, } result = LookupCommandInfo(commandName, commandTypes, searchResolutionOptions, commandOrigin, context); + + if (result != null) + { + break; + } } - if (result != null) + if (result == null && searchResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) { - break; + foreach (var cmdlet in exportedCommands) + { + string abbreviatedCmdlet = new String(cmdlet.Key.Where(c => Char.IsUpper(c) || c == '-').ToArray()); + if (commandName.Equals(abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase)) + { + result = LookupCommandInfo(cmdlet.Key, commandOrigin, context); + if (result != null) + { + break; + } + } + } } } @@ -1474,13 +1490,16 @@ private static void InitPathExtCache(string pathExt) /// /// True if we should search all scopes, false if we should stop after finding the first. /// + /// + /// True if we should try using abbreviation expansion search. + /// /// /// The CmdletInfo for the cmdlet for all the cmdlets with the specified name. /// /// /// If is null or empty. /// - internal IEnumerator GetCmdletInfo(string cmdletName, bool searchAllScopes) + internal IEnumerator GetCmdletInfo(string cmdletName, bool searchAllScopes, bool useAbbreviationExpansion = false) { Dbg.Assert(!string.IsNullOrEmpty(cmdletName), "Caller should verify the cmdletName"); @@ -1502,7 +1521,24 @@ internal IEnumerator GetCmdletInfo(string cmdletName, bool searchAll List cmdlets; if (!scope.CmdletTable.TryGetValue(commandName.ShortName, out cmdlets)) { - continue; + if (useAbbreviationExpansion) + { + foreach (List cmdletList in scope.CmdletTable.Values) + { + foreach (CmdletInfo cmdletInfo in cmdletList) + { + string abbreviatedCmdlet = new String(cmdletInfo.Name.Where(c => Char.IsUpper(c)).ToArray()); + if (cmdletName.Equals(abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase)) + { + yield return cmdletInfo; + } + } + } + } + else + { + continue; + } } foreach (var cmdletInfo in cmdlets) diff --git a/src/System.Management.Automation/engine/CommandSearcher.cs b/src/System.Management.Automation/engine/CommandSearcher.cs index e1eb9e54902..31701ec7694 100644 --- a/src/System.Management.Automation/engine/CommandSearcher.cs +++ b/src/System.Management.Automation/engine/CommandSearcher.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; +using System.Linq; using Dbg = System.Management.Automation.Diagnostics; namespace System.Management.Automation @@ -725,7 +726,8 @@ private CommandInfo GetNextFunction() { CommandInfo result = null; - if ((_commandResolutionOptions & SearchResolutionOptions.ResolveFunctionPatterns) != 0) + if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveFunctionPatterns) || + _commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) { if (_matchingFunctionEnumerator == null) { @@ -745,13 +747,21 @@ private CommandInfo GetNextFunction() { matchingFunction.Add((CommandInfo)functionEntry.Value); } + else if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) + { + string abbreviatedFunction = new String(((string)functionEntry.Key).Where(c => Char.IsUpper(c) || c == '-').ToArray()); + if (_commandName.Equals(abbreviatedFunction, StringComparison.OrdinalIgnoreCase)) + { + matchingFunction.Add((CommandInfo)functionEntry.Value); + } + } } // Process functions from modules - CommandInfo c = GetFunctionFromModules(_commandName); - if (c != null) + CommandInfo cmdInfo = GetFunctionFromModules(_commandName); + if (cmdInfo != null) { - matchingFunction.Add(c); + matchingFunction.Add(cmdInfo); } _matchingFunctionEnumerator = matchingFunction.GetEnumerator(); @@ -949,14 +959,14 @@ private CmdletInfo GetNextCmdlet() if (_matchingCmdlet == null) { - if ((_commandResolutionOptions & SearchResolutionOptions.CommandNameIsPattern) != 0) + if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.CommandNameIsPattern) || _commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) { Collection matchingCmdletInfo = new Collection(); PSSnapinQualifiedName PSSnapinQualifiedCommandName = PSSnapinQualifiedName.GetInstance(_commandName); - if (PSSnapinQualifiedCommandName == null) + if (!_commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion) && PSSnapinQualifiedCommandName == null) { return null; } @@ -984,6 +994,14 @@ private CmdletInfo GetNextCmdlet() matchingCmdletInfo.Add(cmdlet); } } + else if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) + { + string abbreviatedCmdlet = new String(cmdlet.Name.Where(c => Char.IsUpper(c) || c == '-').ToArray()); + if (_commandName.Equals(abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase)) + { + matchingCmdletInfo.Add(cmdlet); + } + } } } @@ -992,7 +1010,8 @@ private CmdletInfo GetNextCmdlet() else { _matchingCmdlet = _context.CommandDiscovery.GetCmdletInfo(_commandName, - (_commandResolutionOptions & SearchResolutionOptions.SearchAllScopes) != 0); + _commandResolutionOptions.HasFlag(SearchResolutionOptions.SearchAllScopes), + _commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)); } } @@ -1588,5 +1607,10 @@ internal enum SearchResolutionOptions /// Use fuzzy matching. FuzzyMatch = 0x10, + + /// + /// Enable searching for cmdlets/functions by abbreviation expansion. + /// + UseAbbreviationExpansion = 0x20, } } diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index 79554d4d7b0..bf3aafff877 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -91,7 +91,11 @@ static ExperimentalFeature() new ExperimentalFeature(name: "PSImplicitRemotingBatching", description: "Batch implicit remoting proxy commands to improve performance", source: EngineSource, - isEnabled: false) + isEnabled: false), + new ExperimentalFeature(name: "PSUseAbbreviationExpansion", + description: "Allow tab completion of cmdlets and functions by abbreviation", + source: EngineSource, + isEnabled: false), }; EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures); diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index 236ff00b1be..268d4fa6790 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -358,6 +358,14 @@ public PSTypeName[] ParameterType private List _commandScores = new List(); + /// The parameter that determines if return cmdlets based on abbreviation expansion. + /// This means it matches cmdlets where the uppercase characters for the noun match + /// the given characters. i.e., Get-sgc would match Get-SomeGreatCmdlet + /// + [Experimental("PSUseAbbreviationExpansion", ExperimentAction.Show)] + [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "AllCommandSet")] + public SwitchParameter UseAbbreviationExpansion { get; set; } + #endregion Definitions of cmdlet parameters #region Overrides @@ -701,6 +709,11 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) options = SearchResolutionOptions.SearchAllScopes; } + if (UseAbbreviationExpansion) + { + options |= SearchResolutionOptions.UseAbbreviationExpansion; + } + if ((this.CommandType & CommandTypes.Alias) != 0) { options |= SearchResolutionOptions.ResolveAliasPatterns; @@ -762,7 +775,7 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) try { - CommandDiscovery.LookupCommandInfo(tempCommandName, this.MyInvocation.CommandOrigin, this.Context); + CommandDiscovery.LookupCommandInfo(tempCommandName, CommandTypes.All, options, this.MyInvocation.CommandOrigin, this.Context); } catch (CommandNotFoundException) { @@ -964,7 +977,7 @@ private bool FindCommandForName(SearchResolutionOptions options, string commandN // Only for this case, the loop should exit // Get-Command Foo - if (isPattern || All || TotalCount != -1 || _isCommandTypeSpecified || _isModuleSpecified || _isFullyQualifiedModuleSpecified) + if (UseAbbreviationExpansion || isPattern || All || TotalCount != -1 || _isCommandTypeSpecified || _isModuleSpecified || _isFullyQualifiedModuleSpecified) { continue; } diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 858173cf8e4..083c01879a7 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -10,6 +10,35 @@ Describe "TabCompletion" -Tags CI { $res.CompletionMatches[0].CompletionText | Should -BeExactly 'Get-Command' } + Context "ExperimentalFeatures" { + + BeforeAll { + $configFilePath = Join-Path $testdrive "useabbreviationexpansion.json" + + @" + { + "ExperimentalFeatures": [ + "PSUseAbbreviationExpansion" + ] + } +"@ > $configFilePath + + } + + It 'Should complete abbreviated cmdlet' { + $res = pwsh -settingsfile $configFilePath -c "(TabExpansion2 -inputScript 'i-psdf' -cursorColumn 'pschr'.Length).CompletionMatches.CompletionText" + $res | Should -HaveCount 1 + $res | Should -BeExactly 'Import-PowerShellDataFile' + } + + It 'Should complete abbreviated function' { + $res = pwsh -settingsfile $configFilePath -c "(TabExpansion2 -inputScript 'pschrl' -cursorColumn 'pschr'.Length).CompletionMatches.CompletionText" + $res | Should -HaveCount 1 + $res | Should -BeExactly 'PSConsoleHostReadLine' + } + + } + It 'Should complete native exe' -Skip:(!$IsWindows) { $res = TabExpansion2 -inputScript 'notep' -cursorColumn 'notep'.Length $res.CompletionMatches[0].CompletionText | Should -BeExactly 'notepad.exe' diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 index 83b255c2d4c..74a6e33287a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 @@ -20,4 +20,70 @@ Describe "Get-Command CI tests" -Tag Feature { $cmds.Name | Should -Contain $ping } } + + Context "-UseAbbreviationExpansion tests" { + BeforeAll { + $configFilePath = Join-Path $testdrive "useabbreviationexpansion.json" + + @" + { + "ExperimentalFeatures": [ + "PSUseAbbreviationExpansion" + ] + } +"@ > $configFilePath + + } + + It "Valid cmdlets works with name and module " -TestCases @( + @{ Name = "i-psdf"; expected = "Import-PowerShellDataFile"; module = $null }, + @{ Name = "i-psdf"; expected = "Import-PowerShellDataFile"; module = "Microsoft.PowerShell.Utility" }, + @{ Name = "r-psb" ; expected = "Remove-PSBreakpoint" ; module = $null }, + @{ Name = "r-psb" ; expected = "Remove-PSBreakpoint" ; module = "Microsoft.PowerShell.Utility" } + ) { + param($name, $expected, $module) + + $command = "Get-Command $name -UseAbbreviationExpansion" + + if ($module) { + $command += " -Module $module" + } + + $results = pwsh -settingsfile $configFilePath -c "$command | ConvertTo-Json" | ConvertFrom-Json + $results | Should -HaveCount 1 + $results.Name | Should -BeExactly $expected + } + + It "Can return multiple results for cmdlets matching abbreviation" { + $results = pwsh -settingsfile $configFilePath -c "Get-Command i-C -UseAbbreviationExpansion | ConvertTo-Json" | ConvertFrom-Json + $results | Should -HaveCount 3 + $results[0].Name | Should -BeExactly "Invoke-Command" + $results[1].Name | Should -BeExactly "Import-Clixml" + $results[2].Name | Should -BeExactly "Import-Csv" + } + + It "Will return multiple results for functions matching abbreviation" { + $manifestPath = Join-Path $testdrive "test.psd1" + $modulePath = Join-Path $testdrive "test.psm1" + + New-ModuleManifest -Path $manifestPath -FunctionsToExport "Get-FooBar","Get-FB" -RootModule test.psm1 + @" + function Get-FooBar { "foobar" } + function Get-FB { "fb" } +"@ > $modulePath + + $results = pwsh -settingsfile $configFilePath -c "Import-Module $manifestPath; Get-Command g-fb -UseAbbreviationExpansion | ConvertTo-Json" | ConvertFrom-Json + $results | Should -HaveCount 2 + $results[0].Name | Should -BeExactly "Get-FB" + $results[1].Name | Should -BeExactly "Get-FooBar" + } + + It "Non-existing cmdlets returns non-terminating error" { + pwsh -settingsfile $configFilePath -c 'try { get-command g-adf -ea stop } catch { $_.fullyqualifiederrorid }' | Should -BeExactly "CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand" + } + + It "No results if wildcard is used" { + pwsh -settingsfile $configFilePath -c Get-Command i-psd* -UseAbbreviationExpansion | Should -BeNullOrEmpty + } + } } From 9f3dfe95a9f41568f5613f310ed9407374402439 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 23 Oct 2018 15:28:47 -0700 Subject: [PATCH 2/7] [feature] address codefactor issues --- src/System.Management.Automation/engine/CommandDiscovery.cs | 5 ++--- src/System.Management.Automation/engine/CommandSearcher.cs | 4 ++-- src/System.Management.Automation/engine/GetCommandCommand.cs | 5 +++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandDiscovery.cs b/src/System.Management.Automation/engine/CommandDiscovery.cs index 92c4d1f5a19..e212126d53e 100644 --- a/src/System.Management.Automation/engine/CommandDiscovery.cs +++ b/src/System.Management.Automation/engine/CommandDiscovery.cs @@ -1142,7 +1142,7 @@ private static CommandInfo TryModuleAutoDiscovery(string commandName, { foreach (var cmdlet in exportedCommands) { - string abbreviatedCmdlet = new String(cmdlet.Key.Where(c => Char.IsUpper(c) || c == '-').ToArray()); + string abbreviatedCmdlet = new string(cmdlet.Key.Where(c => char.IsUpper(c) || c == '-').ToArray()); if (commandName.Equals(abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase)) { result = LookupCommandInfo(cmdlet.Key, commandOrigin, context); @@ -1512,7 +1512,6 @@ internal IEnumerator GetCmdletInfo(string cmdletName, bool searchAll // Check the current cmdlet cache then check the top level // if we aren't already at the top level. - SessionStateScopeEnumerator scopeEnumerator = new SessionStateScopeEnumerator(Context.EngineSessionState.CurrentScope); @@ -1527,7 +1526,7 @@ internal IEnumerator GetCmdletInfo(string cmdletName, bool searchAll { foreach (CmdletInfo cmdletInfo in cmdletList) { - string abbreviatedCmdlet = new String(cmdletInfo.Name.Where(c => Char.IsUpper(c)).ToArray()); + string abbreviatedCmdlet = new string(cmdletInfo.Name.Where(c => char.IsUpper(c)).ToArray()); if (cmdletName.Equals(abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase)) { yield return cmdletInfo; diff --git a/src/System.Management.Automation/engine/CommandSearcher.cs b/src/System.Management.Automation/engine/CommandSearcher.cs index 31701ec7694..b97fc3ab02e 100644 --- a/src/System.Management.Automation/engine/CommandSearcher.cs +++ b/src/System.Management.Automation/engine/CommandSearcher.cs @@ -749,7 +749,7 @@ private CommandInfo GetNextFunction() } else if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) { - string abbreviatedFunction = new String(((string)functionEntry.Key).Where(c => Char.IsUpper(c) || c == '-').ToArray()); + string abbreviatedFunction = new string(((string)functionEntry.Key).Where(c => char.IsUpper(c) || c == '-').ToArray()); if (_commandName.Equals(abbreviatedFunction, StringComparison.OrdinalIgnoreCase)) { matchingFunction.Add((CommandInfo)functionEntry.Value); @@ -996,7 +996,7 @@ private CmdletInfo GetNextCmdlet() } else if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) { - string abbreviatedCmdlet = new String(cmdlet.Name.Where(c => Char.IsUpper(c) || c == '-').ToArray()); + string abbreviatedCmdlet = new string(cmdlet.Name.Where(c => char.IsUpper(c) || c == '-').ToArray()); if (_commandName.Equals(abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase)) { matchingCmdletInfo.Add(cmdlet); diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index 268d4fa6790..cb494b87a03 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -358,9 +358,10 @@ public PSTypeName[] ParameterType private List _commandScores = new List(); - /// The parameter that determines if return cmdlets based on abbreviation expansion. + /// + /// Gets or sets the parameter that determines if return cmdlets based on abbreviation expansion. /// This means it matches cmdlets where the uppercase characters for the noun match - /// the given characters. i.e., Get-sgc would match Get-SomeGreatCmdlet + /// the given characters. i.e., g-sgc would match Get-SomeGreatCmdlet. /// [Experimental("PSUseAbbreviationExpansion", ExperimentAction.Show)] [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "AllCommandSet")] From 2b6365601fa8d0a75ee0bb630bc925a01c12f2b5 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 8 Jan 2019 10:24:42 -0800 Subject: [PATCH 3/7] address Jim's feedback --- .../engine/CommandCompletion/CompletionCompleters.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index d4bfe71b682..fbebcbb7c3e 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -164,6 +164,7 @@ private static List ExecuteGetCommandCommand(CompletionContext powershell.AddParameter("CommandType", types); } + // Exception is ignored, the user simply does not get any completion results if the pipeline fails Exception exceptionThrown; var commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); From c2530b0c5ff7705f6f6fe922d443596353bb9c95 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 9 Jan 2019 11:22:28 -0800 Subject: [PATCH 4/7] address Dongbo's feedback fixed code to support returning multiple results and not just first one added test for automoduleloading which also covers multiple results from different modules --- .../CommandCompletion/CompletionCompleters.cs | 8 ++--- .../engine/CommandDiscovery.cs | 31 +++++------------ .../engine/CommandSearcher.cs | 6 ++-- .../engine/GetCommandCommand.cs | 16 ++++----- .../engine/Modules/ModuleUtils.cs | 12 +++++-- .../Get-Command.Tests.ps1 | 33 ++++++++++++++----- 6 files changed, 56 insertions(+), 50 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index fbebcbb7c3e..730a3736e92 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -176,10 +176,10 @@ private static List ExecuteGetCommandCommand(CompletionContext .AddParameter("All") .AddParameter("Name", commandName); - if (ExperimentalFeature.IsEnabled("PSUseAbbreviationExpansion")) - { - powershell.AddParameter("UseAbbreviationExpansion"); - } + if (ExperimentalFeature.IsEnabled("PSUseAbbreviationExpansion")) + { + powershell.AddParameter("UseAbbreviationExpansion"); + } if (moduleName != null) { diff --git a/src/System.Management.Automation/engine/CommandDiscovery.cs b/src/System.Management.Automation/engine/CommandDiscovery.cs index e212126d53e..9d0623695be 100644 --- a/src/System.Management.Automation/engine/CommandDiscovery.cs +++ b/src/System.Management.Automation/engine/CommandDiscovery.cs @@ -1138,13 +1138,18 @@ private static CommandInfo TryModuleAutoDiscovery(string commandName, } } - if (result == null && searchResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) + if (result != null) + { + break; + } + else if (searchResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) { foreach (var cmdlet in exportedCommands) { string abbreviatedCmdlet = new string(cmdlet.Key.Where(c => char.IsUpper(c) || c == '-').ToArray()); if (commandName.Equals(abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase)) { + // take only the first found command result = LookupCommandInfo(cmdlet.Key, commandOrigin, context); if (result != null) { @@ -1490,16 +1495,13 @@ private static void InitPathExtCache(string pathExt) /// /// True if we should search all scopes, false if we should stop after finding the first. /// - /// - /// True if we should try using abbreviation expansion search. - /// /// /// The CmdletInfo for the cmdlet for all the cmdlets with the specified name. /// /// /// If is null or empty. /// - internal IEnumerator GetCmdletInfo(string cmdletName, bool searchAllScopes, bool useAbbreviationExpansion = false) + internal IEnumerator GetCmdletInfo(string cmdletName, bool searchAllScopes) { Dbg.Assert(!string.IsNullOrEmpty(cmdletName), "Caller should verify the cmdletName"); @@ -1520,24 +1522,7 @@ internal IEnumerator GetCmdletInfo(string cmdletName, bool searchAll List cmdlets; if (!scope.CmdletTable.TryGetValue(commandName.ShortName, out cmdlets)) { - if (useAbbreviationExpansion) - { - foreach (List cmdletList in scope.CmdletTable.Values) - { - foreach (CmdletInfo cmdletInfo in cmdletList) - { - string abbreviatedCmdlet = new string(cmdletInfo.Name.Where(c => char.IsUpper(c)).ToArray()); - if (cmdletName.Equals(abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase)) - { - yield return cmdletInfo; - } - } - } - } - else - { - continue; - } + continue; } foreach (var cmdletInfo in cmdlets) diff --git a/src/System.Management.Automation/engine/CommandSearcher.cs b/src/System.Management.Automation/engine/CommandSearcher.cs index b97fc3ab02e..4158ac31234 100644 --- a/src/System.Management.Automation/engine/CommandSearcher.cs +++ b/src/System.Management.Automation/engine/CommandSearcher.cs @@ -726,8 +726,7 @@ private CommandInfo GetNextFunction() { CommandInfo result = null; - if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveFunctionPatterns) || - _commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) + if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveFunctionPatterns)) { if (_matchingFunctionEnumerator == null) { @@ -1010,8 +1009,7 @@ private CmdletInfo GetNextCmdlet() else { _matchingCmdlet = _context.CommandDiscovery.GetCmdletInfo(_commandName, - _commandResolutionOptions.HasFlag(SearchResolutionOptions.SearchAllScopes), - _commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)); + _commandResolutionOptions.HasFlag(SearchResolutionOptions.SearchAllScopes)); } } diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index cb494b87a03..deb55dcf807 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -715,6 +715,11 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) options |= SearchResolutionOptions.UseAbbreviationExpansion; } + if (UseFuzzyMatching) + { + options |= SearchResolutionOptions.FuzzyMatch; + } + if ((this.CommandType & CommandTypes.Alias) != 0) { options |= SearchResolutionOptions.ResolveAliasPatterns; @@ -742,13 +747,7 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) moduleName = this.Module[0]; } - bool isPattern = WildcardPattern.ContainsWildcardCharacters(plainCommandName); - if (UseFuzzyMatching) - { - options |= SearchResolutionOptions.FuzzyMatch; - isPattern = true; - } - + bool isPattern = WildcardPattern.ContainsWildcardCharacters(plainCommandName) || UseAbbreviationExpansion || UseFuzzyMatching; if (isPattern) { options |= SearchResolutionOptions.CommandNameIsPattern; @@ -812,7 +811,8 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) this.Context, this.MyInvocation.CommandOrigin, rediscoverImportedModules: true, - moduleVersionRequired: _isFullyQualifiedModuleSpecified); + moduleVersionRequired: _isFullyQualifiedModuleSpecified, + useAbbreviationExpansion: UseAbbreviationExpansion); } foreach (CommandInfo command in commands) diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index 9214f56186e..a878e56723d 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Management.Automation.Runspaces; using Dbg = System.Management.Automation.Diagnostics; @@ -411,8 +412,9 @@ internal static IEnumerable GetFuzzyMatchingCommands(string patter /// If true, rediscovers imported modules. /// Specific module version to be required. /// Use fuzzy matching. + /// Use abbreviation expansion for matching. /// Returns CommandInfo IEnumerable. - internal static IEnumerable GetMatchingCommands(string pattern, ExecutionContext context, CommandOrigin commandOrigin, bool rediscoverImportedModules = false, bool moduleVersionRequired = false, bool useFuzzyMatching = false) + internal static IEnumerable GetMatchingCommands(string pattern, ExecutionContext context, CommandOrigin commandOrigin, bool rediscoverImportedModules = false, bool moduleVersionRequired = false, bool useFuzzyMatching = false, bool useAbbreviationExpansion = false) { // Otherwise, if it had wildcards, just return the "AvailableCommand" // type of command info. @@ -449,8 +451,10 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe foreach (KeyValuePair entry in psModule.ExportedCommands) { + string abbreviatedCmdlet = new string(entry.Value.Name.Where(c => char.IsUpper(c) || c == '-').ToArray()); if (commandPattern.IsMatch(entry.Value.Name) || - (useFuzzyMatching && FuzzyMatcher.IsFuzzyMatch(entry.Value.Name, pattern))) + (useFuzzyMatching && FuzzyMatcher.IsFuzzyMatch(entry.Value.Name, pattern)) || + (useAbbreviationExpansion && string.Equals(pattern, abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase))) { CommandInfo current = null; switch (entry.Value.CommandType) @@ -507,9 +511,11 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe { string commandName = pair.Key; CommandTypes commandTypes = pair.Value; + string abbreviatedCmdlet = new string(commandName.Where(c => char.IsUpper(c) || c == '-').ToArray()); if (commandPattern.IsMatch(commandName) || - (useFuzzyMatching && FuzzyMatcher.IsFuzzyMatch(commandName, pattern))) + (useFuzzyMatching && FuzzyMatcher.IsFuzzyMatch(commandName, pattern)) || + (useAbbreviationExpansion && string.Equals(pattern, abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase))) { bool shouldExportCommand = true; diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 index 74a6e33287a..c9d7e030709 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 @@ -23,6 +23,15 @@ Describe "Get-Command CI tests" -Tag Feature { Context "-UseAbbreviationExpansion tests" { BeforeAll { + $testModulesPath = Join-Path $testdrive "Modules" + $testPSModulePath = [System.IO.Path]::PathSeparator + $testModulesPath + $null = New-Item -ItemType Directory -Path $testModulesPath + $null = New-Item -ItemType Directory -Path (Join-Path $testModulesPath "test1") + $null = New-Item -ItemType Directory -Path (Join-Path $testModulesPath "test2") + + Set-Content -Path (Join-Path $testModulesPath "test1/test1.psm1") -Value "function Import-FooZedZed {}" + Set-Content -Path (Join-Path $testModulesPath "test2/test2.psm1") -Value "function Invoke-FooZedZed {}" + $configFilePath = Join-Path $testdrive "useabbreviationexpansion.json" @" @@ -35,6 +44,13 @@ Describe "Get-Command CI tests" -Tag Feature { } + It "Can return multiple results relying on auto module loading" { + $results = pwsh -outputformat xml -settingsfile $configFilePath -command "`$env:PSModulePath += '$testPSModulePath'; Get-Command i-fzz -UseAbbreviationExpansion" + $results | Should -HaveCount 2 + $results.Name | Should -Contain "Invoke-FooZedZed" + $results.Name | Should -Contain "Import-FooZedZed" + } + It "Valid cmdlets works with name and module " -TestCases @( @{ Name = "i-psdf"; expected = "Import-PowerShellDataFile"; module = $null }, @{ Name = "i-psdf"; expected = "Import-PowerShellDataFile"; module = "Microsoft.PowerShell.Utility" }, @@ -49,17 +65,18 @@ Describe "Get-Command CI tests" -Tag Feature { $command += " -Module $module" } - $results = pwsh -settingsfile $configFilePath -c "$command | ConvertTo-Json" | ConvertFrom-Json + $results = pwsh -outputformat xml -settingsfile $configFilePath -command "$command" $results | Should -HaveCount 1 $results.Name | Should -BeExactly $expected } It "Can return multiple results for cmdlets matching abbreviation" { - $results = pwsh -settingsfile $configFilePath -c "Get-Command i-C -UseAbbreviationExpansion | ConvertTo-Json" | ConvertFrom-Json + # use mixed casing to validate case insensitivity + $results = pwsh -outputformat xml -settingsfile $configFilePath -command "Get-Command i-C -UseAbbreviationExpansion" $results | Should -HaveCount 3 - $results[0].Name | Should -BeExactly "Invoke-Command" - $results[1].Name | Should -BeExactly "Import-Clixml" - $results[2].Name | Should -BeExactly "Import-Csv" + $results.Name | Should -Contain "Invoke-Command" + $results.Name | Should -Contain "Import-Clixml" + $results.Name | Should -Contain "Import-Csv" } It "Will return multiple results for functions matching abbreviation" { @@ -72,18 +89,18 @@ Describe "Get-Command CI tests" -Tag Feature { function Get-FB { "fb" } "@ > $modulePath - $results = pwsh -settingsfile $configFilePath -c "Import-Module $manifestPath; Get-Command g-fb -UseAbbreviationExpansion | ConvertTo-Json" | ConvertFrom-Json + $results = pwsh -outputformat xml -settingsfile $configFilePath -command "Import-Module $manifestPath; Get-Command g-fb -UseAbbreviationExpansion" $results | Should -HaveCount 2 $results[0].Name | Should -BeExactly "Get-FB" $results[1].Name | Should -BeExactly "Get-FooBar" } It "Non-existing cmdlets returns non-terminating error" { - pwsh -settingsfile $configFilePath -c 'try { get-command g-adf -ea stop } catch { $_.fullyqualifiederrorid }' | Should -BeExactly "CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand" + pwsh -settingsfile $configFilePath -command 'try { get-command g-adf -ea stop } catch { $_.fullyqualifiederrorid }' | Should -BeExactly "CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand" } It "No results if wildcard is used" { - pwsh -settingsfile $configFilePath -c Get-Command i-psd* -UseAbbreviationExpansion | Should -BeNullOrEmpty + pwsh -settingsfile $configFilePath -command Get-Command i-psd* -UseAbbreviationExpansion | Should -BeNullOrEmpty } } } From 9339cd052788b18a1c77b33bf45b6296bd892826 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 9 Jan 2019 12:09:03 -0800 Subject: [PATCH 5/7] refactor ExecuteGetCommandCommand as local function --- .../CommandCompletion/CompletionCompleters.cs | 134 +++++++++--------- .../TabCompletion/TabCompletion.Tests.ps1 | 2 +- 2 files changed, 66 insertions(+), 70 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 730a3736e92..5d2aeb998af 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -82,6 +82,69 @@ internal static List CompleteCommand(CompletionContext context private static List CompleteCommand(CompletionContext context, string moduleName, CommandTypes types = CommandTypes.All) { + List ExecuteGetCommandCommand(string command, CompletionContext completionContext, string module, CommandTypes commandTypes, bool useModulePrefix, bool addAmpersand, string quotes) + { + var powershell = completionContext.Helper + .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) + .AddParameter("All") + .AddParameter("Name", command + "*"); + + if (module != null) + { + powershell.AddParameter("Module", module); + } + + if (!commandTypes.Equals(CommandTypes.All)) + { + powershell.AddParameter("CommandType", commandTypes); + } + + // Exception is ignored, the user simply does not get any completion results if the pipeline fails + Exception exceptionThrown; + var commandInfos = completionContext.Helper.ExecuteCurrentPowerShell(out exceptionThrown); + + if (commandInfos == null || commandInfos.Count == 0) + { + powershell.Commands.Clear(); + powershell + .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) + .AddParameter("All") + .AddParameter("Name", command); + + if (ExperimentalFeature.IsEnabled("PSUseAbbreviationExpansion")) + { + powershell.AddParameter("UseAbbreviationExpansion"); + } + + if (module != null) + { + powershell.AddParameter("Module", module); + } + + if (!commandTypes.Equals(CommandTypes.All)) + { + powershell.AddParameter("CommandType", commandTypes); + } + + commandInfos = completionContext.Helper.ExecuteCurrentPowerShell(out exceptionThrown); + } + + List completionResults = null; + + if (commandInfos != null && commandInfos.Count > 1) + { + // OrderBy is using stable sorting + var sortedCommandInfos = commandInfos.OrderBy(a => a, new CommandNameComparer()); + completionResults = MakeCommandsUnique(sortedCommandInfos, useModulePrefix, addAmpersand, quotes); + } + else + { + completionResults = MakeCommandsUnique(commandInfos, useModulePrefix, addAmpersand, quotes); + } + + return completionResults; + } + var addAmpersandIfNecessary = IsAmpersandNeeded(context, false); string commandName = context.WordToComplete; @@ -99,7 +162,7 @@ private static List CompleteCommand(CompletionContext context, lastAst = context.RelatedAsts.Last(); } - commandResults = ExecuteGetCommandCommand(context, moduleName, types, includeModulePrefix: false); + commandResults = ExecuteGetCommandCommand(commandName, context, moduleName, types, useModulePrefix: false, addAmpersandIfNecessary, quote); if (lastAst != null) { @@ -136,75 +199,8 @@ private static List CompleteCommand(CompletionContext context, moduleName = commandName.Substring(0, indexOfFirstBackslash); commandName = commandName.Substring(indexOfFirstBackslash + 1); - commandResults = ExecuteGetCommandCommand(context, moduleName, types, includeModulePrefix: true); - } - } - - return commandResults; - } - - private static List ExecuteGetCommandCommand(CompletionContext context, string moduleName, CommandTypes types, bool includeModulePrefix) - { - var addAmpersandIfNecessary = IsAmpersandNeeded(context, false); - string commandName = context.WordToComplete; - string quote = HandleDoubleAndSingleQuote(ref commandName); - - var powershell = context.Helper - .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) - .AddParameter("All") - .AddParameter("Name", commandName + "*"); - - if (moduleName != null) - { - powershell.AddParameter("Module", moduleName); - } - - if (!types.Equals(CommandTypes.All)) - { - powershell.AddParameter("CommandType", types); - } - - // Exception is ignored, the user simply does not get any completion results if the pipeline fails - Exception exceptionThrown; - var commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); - - if (commandInfos == null || commandInfos.Count == 0) - { - powershell.Commands.Clear(); - powershell - .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) - .AddParameter("All") - .AddParameter("Name", commandName); - - if (ExperimentalFeature.IsEnabled("PSUseAbbreviationExpansion")) - { - powershell.AddParameter("UseAbbreviationExpansion"); - } - - if (moduleName != null) - { - powershell.AddParameter("Module", moduleName); - } - - if (!types.Equals(CommandTypes.All)) - { - powershell.AddParameter("CommandType", types); + commandResults = ExecuteGetCommandCommand(commandName, context, moduleName, types, useModulePrefix: true, addAmpersandIfNecessary, quote); } - - commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); - } - - List commandResults = null; - - if (commandInfos != null && commandInfos.Count > 1) - { - // OrderBy is using stable sorting - var sortedCommandInfos = commandInfos.OrderBy(a => a, new CommandNameComparer()); - commandResults = MakeCommandsUnique(sortedCommandInfos, includeModulePrefix, addAmpersandIfNecessary, quote); - } - else - { - commandResults = MakeCommandsUnique(commandInfos, includeModulePrefix, addAmpersandIfNecessary, quote); } return commandResults; diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 083c01879a7..88a4b85dcae 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -33,7 +33,7 @@ Describe "TabCompletion" -Tags CI { It 'Should complete abbreviated function' { $res = pwsh -settingsfile $configFilePath -c "(TabExpansion2 -inputScript 'pschrl' -cursorColumn 'pschr'.Length).CompletionMatches.CompletionText" - $res | Should -HaveCount 1 + $res.Count | Should -BeGreaterOrEqual 1 $res | Should -BeExactly 'PSConsoleHostReadLine' } From cebe96500a6af9d6ec60d7a9fb70c8aa555b372c Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 9 Jan 2019 21:21:56 -0800 Subject: [PATCH 6/7] [feature] address Dongbo's feedback --- .../CommandCompletion/CompletionCompleters.cs | 130 +++++++++--------- .../engine/CommandDiscovery.cs | 21 --- .../engine/CommandSearcher.cs | 14 +- .../engine/GetCommandCommand.cs | 2 +- .../engine/Modules/ModuleUtils.cs | 28 +++- 5 files changed, 96 insertions(+), 99 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 5d2aeb998af..27166bc790b 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -82,69 +82,6 @@ internal static List CompleteCommand(CompletionContext context private static List CompleteCommand(CompletionContext context, string moduleName, CommandTypes types = CommandTypes.All) { - List ExecuteGetCommandCommand(string command, CompletionContext completionContext, string module, CommandTypes commandTypes, bool useModulePrefix, bool addAmpersand, string quotes) - { - var powershell = completionContext.Helper - .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) - .AddParameter("All") - .AddParameter("Name", command + "*"); - - if (module != null) - { - powershell.AddParameter("Module", module); - } - - if (!commandTypes.Equals(CommandTypes.All)) - { - powershell.AddParameter("CommandType", commandTypes); - } - - // Exception is ignored, the user simply does not get any completion results if the pipeline fails - Exception exceptionThrown; - var commandInfos = completionContext.Helper.ExecuteCurrentPowerShell(out exceptionThrown); - - if (commandInfos == null || commandInfos.Count == 0) - { - powershell.Commands.Clear(); - powershell - .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) - .AddParameter("All") - .AddParameter("Name", command); - - if (ExperimentalFeature.IsEnabled("PSUseAbbreviationExpansion")) - { - powershell.AddParameter("UseAbbreviationExpansion"); - } - - if (module != null) - { - powershell.AddParameter("Module", module); - } - - if (!commandTypes.Equals(CommandTypes.All)) - { - powershell.AddParameter("CommandType", commandTypes); - } - - commandInfos = completionContext.Helper.ExecuteCurrentPowerShell(out exceptionThrown); - } - - List completionResults = null; - - if (commandInfos != null && commandInfos.Count > 1) - { - // OrderBy is using stable sorting - var sortedCommandInfos = commandInfos.OrderBy(a => a, new CommandNameComparer()); - completionResults = MakeCommandsUnique(sortedCommandInfos, useModulePrefix, addAmpersand, quotes); - } - else - { - completionResults = MakeCommandsUnique(commandInfos, useModulePrefix, addAmpersand, quotes); - } - - return completionResults; - } - var addAmpersandIfNecessary = IsAmpersandNeeded(context, false); string commandName = context.WordToComplete; @@ -162,7 +99,7 @@ List ExecuteGetCommandCommand(string command, CompletionContex lastAst = context.RelatedAsts.Last(); } - commandResults = ExecuteGetCommandCommand(commandName, context, moduleName, types, useModulePrefix: false, addAmpersandIfNecessary, quote); + commandResults = ExecuteGetCommandCommand(useModulePrefix: false); if (lastAst != null) { @@ -199,11 +136,74 @@ List ExecuteGetCommandCommand(string command, CompletionContex moduleName = commandName.Substring(0, indexOfFirstBackslash); commandName = commandName.Substring(indexOfFirstBackslash + 1); - commandResults = ExecuteGetCommandCommand(commandName, context, moduleName, types, useModulePrefix: true, addAmpersandIfNecessary, quote); + commandResults = ExecuteGetCommandCommand(useModulePrefix: true); } } return commandResults; + + List ExecuteGetCommandCommand(bool useModulePrefix) + { + var powershell = context.Helper + .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) + .AddParameter("All") + .AddParameter("Name", commandName + "*"); + + if (moduleName != null) + { + powershell.AddParameter("Module", moduleName); + } + + if (!types.Equals(CommandTypes.All)) + { + powershell.AddParameter("CommandType", types); + } + + // Exception is ignored, the user simply does not get any completion results if the pipeline fails + Exception exceptionThrown; + var commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); + + if (commandInfos == null || commandInfos.Count == 0) + { + powershell.Commands.Clear(); + powershell + .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) + .AddParameter("All") + .AddParameter("Name", commandName); + + if (ExperimentalFeature.IsEnabled("PSUseAbbreviationExpansion")) + { + powershell.AddParameter("UseAbbreviationExpansion"); + } + + if (moduleName != null) + { + powershell.AddParameter("Module", moduleName); + } + + if (!types.Equals(CommandTypes.All)) + { + powershell.AddParameter("CommandType", types); + } + + commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); + } + + List completionResults = null; + + if (commandInfos != null && commandInfos.Count > 1) + { + // OrderBy is using stable sorting + var sortedCommandInfos = commandInfos.OrderBy(a => a, new CommandNameComparer()); + completionResults = MakeCommandsUnique(sortedCommandInfos, useModulePrefix, addAmpersandIfNecessary, quote); + } + else + { + completionResults = MakeCommandsUnique(commandInfos, useModulePrefix, addAmpersandIfNecessary, quote); + } + + return completionResults; + } } private static readonly HashSet s_keywordsToExcludeFromAddingAmpersand diff --git a/src/System.Management.Automation/engine/CommandDiscovery.cs b/src/System.Management.Automation/engine/CommandDiscovery.cs index 9d0623695be..54b0ac968d0 100644 --- a/src/System.Management.Automation/engine/CommandDiscovery.cs +++ b/src/System.Management.Automation/engine/CommandDiscovery.cs @@ -1131,33 +1131,12 @@ private static CommandInfo TryModuleAutoDiscovery(string commandName, } result = LookupCommandInfo(commandName, commandTypes, searchResolutionOptions, commandOrigin, context); - - if (result != null) - { - break; - } } if (result != null) { break; } - else if (searchResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) - { - foreach (var cmdlet in exportedCommands) - { - string abbreviatedCmdlet = new string(cmdlet.Key.Where(c => char.IsUpper(c) || c == '-').ToArray()); - if (commandName.Equals(abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase)) - { - // take only the first found command - result = LookupCommandInfo(cmdlet.Key, commandOrigin, context); - if (result != null) - { - break; - } - } - } - } } // TODO: this causes AppVeyor builds to fail due to invalid XML being output diff --git a/src/System.Management.Automation/engine/CommandSearcher.cs b/src/System.Management.Automation/engine/CommandSearcher.cs index 4158ac31234..59c97b60c73 100644 --- a/src/System.Management.Automation/engine/CommandSearcher.cs +++ b/src/System.Management.Automation/engine/CommandSearcher.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Management.Automation.Internal; using Dbg = System.Management.Automation.Diagnostics; namespace System.Management.Automation @@ -748,8 +749,7 @@ private CommandInfo GetNextFunction() } else if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) { - string abbreviatedFunction = new string(((string)functionEntry.Key).Where(c => char.IsUpper(c) || c == '-').ToArray()); - if (_commandName.Equals(abbreviatedFunction, StringComparison.OrdinalIgnoreCase)) + if (_commandName.Equals(ModuleUtils.AbbreviateName((string)functionEntry.Key), StringComparison.OrdinalIgnoreCase)) { matchingFunction.Add((CommandInfo)functionEntry.Value); } @@ -955,17 +955,18 @@ private CommandInfo GetFunction(string function) private CmdletInfo GetNextCmdlet() { CmdletInfo result = null; + bool useAbbreviationExpansion = _commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion); if (_matchingCmdlet == null) { - if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.CommandNameIsPattern) || _commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) + if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.CommandNameIsPattern) || useAbbreviationExpansion) { Collection matchingCmdletInfo = new Collection(); PSSnapinQualifiedName PSSnapinQualifiedCommandName = PSSnapinQualifiedName.GetInstance(_commandName); - if (!_commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion) && PSSnapinQualifiedCommandName == null) + if (!useAbbreviationExpansion && PSSnapinQualifiedCommandName == null) { return null; } @@ -993,10 +994,9 @@ private CmdletInfo GetNextCmdlet() matchingCmdletInfo.Add(cmdlet); } } - else if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion)) + else if (useAbbreviationExpansion) { - string abbreviatedCmdlet = new string(cmdlet.Name.Where(c => char.IsUpper(c) || c == '-').ToArray()); - if (_commandName.Equals(abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase)) + if (_commandName.Equals(ModuleUtils.AbbreviateName(cmdlet.Name), StringComparison.OrdinalIgnoreCase)) { matchingCmdletInfo.Add(cmdlet); } diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index deb55dcf807..fd4f4e6cca3 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -978,7 +978,7 @@ private bool FindCommandForName(SearchResolutionOptions options, string commandN // Only for this case, the loop should exit // Get-Command Foo - if (UseAbbreviationExpansion || isPattern || All || TotalCount != -1 || _isCommandTypeSpecified || _isModuleSpecified || _isFullyQualifiedModuleSpecified) + if (isPattern || All || TotalCount != -1 || _isCommandTypeSpecified || _isModuleSpecified || _isFullyQualifiedModuleSpecified) { continue; } diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index a878e56723d..c81174d0b91 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Management.Automation.Runspaces; +using System.Text; using Dbg = System.Management.Automation.Diagnostics; namespace System.Management.Automation.Internal @@ -413,7 +414,7 @@ internal static IEnumerable GetFuzzyMatchingCommands(string patter /// Specific module version to be required. /// Use fuzzy matching. /// Use abbreviation expansion for matching. - /// Returns CommandInfo IEnumerable. + /// 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) { // Otherwise, if it had wildcards, just return the "AvailableCommand" @@ -451,10 +452,9 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe foreach (KeyValuePair entry in psModule.ExportedCommands) { - string abbreviatedCmdlet = new string(entry.Value.Name.Where(c => char.IsUpper(c) || c == '-').ToArray()); if (commandPattern.IsMatch(entry.Value.Name) || (useFuzzyMatching && FuzzyMatcher.IsFuzzyMatch(entry.Value.Name, pattern)) || - (useAbbreviationExpansion && string.Equals(pattern, abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase))) + (useAbbreviationExpansion && string.Equals(pattern, AbbreviateName(entry.Value.Name), StringComparison.OrdinalIgnoreCase))) { CommandInfo current = null; switch (entry.Value.CommandType) @@ -511,11 +511,10 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe { string commandName = pair.Key; CommandTypes commandTypes = pair.Value; - string abbreviatedCmdlet = new string(commandName.Where(c => char.IsUpper(c) || c == '-').ToArray()); if (commandPattern.IsMatch(commandName) || (useFuzzyMatching && FuzzyMatcher.IsFuzzyMatch(commandName, pattern)) || - (useAbbreviationExpansion && string.Equals(pattern, abbreviatedCmdlet, StringComparison.OrdinalIgnoreCase))) + (useAbbreviationExpansion && string.Equals(pattern, AbbreviateName(commandName), StringComparison.OrdinalIgnoreCase))) { bool shouldExportCommand = true; @@ -584,6 +583,25 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe } } } + + /// + /// Returns abbreviated version of a command name. + /// + /// Name of the command to transform. + /// Abbreviated version of the command name. + internal static string AbbreviateName(string commandName) + { + StringBuilder abbreviation = new StringBuilder(); + foreach (char c in commandName) + { + if (char.IsUpper(c) || c == '-') + { + abbreviation.Append(c); + } + } + + return abbreviation.ToString(); + } } internal struct CommandScore From dc7739ee30238db2ba8ce27ebc5f225511e9dba0 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 10 Jan 2019 13:21:17 -0800 Subject: [PATCH 7/7] address Dongbo's feedback --- src/System.Management.Automation/engine/CommandSearcher.cs | 1 - src/System.Management.Automation/engine/GetCommandCommand.cs | 2 +- .../engine/Modules/ModuleUtils.cs | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandSearcher.cs b/src/System.Management.Automation/engine/CommandSearcher.cs index 59c97b60c73..f3f29a514c0 100644 --- a/src/System.Management.Automation/engine/CommandSearcher.cs +++ b/src/System.Management.Automation/engine/CommandSearcher.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; -using System.Linq; using System.Management.Automation.Internal; using Dbg = System.Management.Automation.Diagnostics; diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index fd4f4e6cca3..026bdcd12d2 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -775,7 +775,7 @@ private void AccumulateMatchingCommands(IEnumerable commandNames) try { - CommandDiscovery.LookupCommandInfo(tempCommandName, CommandTypes.All, options, this.MyInvocation.CommandOrigin, this.Context); + CommandDiscovery.LookupCommandInfo(tempCommandName, this.MyInvocation.CommandOrigin, this.Context); } catch (CommandNotFoundException) { diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index c81174d0b91..3bbe2ffa7f8 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using System.Management.Automation.Runspaces; using System.Text; using Dbg = System.Management.Automation.Diagnostics; @@ -591,7 +590,8 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe /// Abbreviated version of the command name. internal static string AbbreviateName(string commandName) { - StringBuilder abbreviation = new StringBuilder(); + // Use default size of 6 which represents expected average abbreviation length + StringBuilder abbreviation = new StringBuilder(6); foreach (char c in commandName) { if (char.IsUpper(c) || c == '-')