From 0e18be6659aad4171736faf7f6b94a835cf6f12e Mon Sep 17 00:00:00 2001 From: Armaan Mcleod Date: Sun, 17 Dec 2023 03:34:51 +1100 Subject: [PATCH] Add `-Verb` argument completer for `Get-Verb`/ `Get-Command` and refactor `Get-Verb` (#20286) --- .../commands/utility/GetVerbCommand.cs | 45 +--- .../engine/GetCommandCommand.cs | 2 + .../utils/Verbs.cs | 238 +++++++++++++++++- .../TabCompletion/TabCompletion.Tests.ps1 | 51 ++++ 4 files changed, 290 insertions(+), 46 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetVerbCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetVerbCommand.cs index 1ec93b0f1d..61a0fe1a39 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetVerbCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetVerbCommand.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.ObjectModel; using System.Management.Automation; -using System.Reflection; +using static System.Management.Automation.Verbs; namespace Microsoft.PowerShell.Commands { @@ -19,6 +17,7 @@ namespace Microsoft.PowerShell.Commands /// Optional Verb filter. /// [Parameter(ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [ArgumentCompleter(typeof(VerbArgumentCompleter))] public string[] Verb { get; set; @@ -39,45 +38,9 @@ namespace Microsoft.PowerShell.Commands /// protected override void ProcessRecord() { - Type[] verbTypes = new Type[] { typeof(VerbsCommon), typeof(VerbsCommunications), typeof(VerbsData), - typeof(VerbsDiagnostic), typeof(VerbsLifecycle), typeof(VerbsOther), typeof(VerbsSecurity) }; - - Collection matchingVerbs = SessionStateUtilities.CreateWildcardsFromStrings( - this.Verb, - WildcardOptions.IgnoreCase - ); - - foreach (Type type in verbTypes) + foreach (VerbInfo verb in FilterByVerbsAndGroups(Verb, Group)) { - string groupName = type.Name.Substring(5); - if (this.Group != null) - { - if (!SessionStateUtilities.CollectionContainsValue(this.Group, groupName, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - } - - foreach (FieldInfo field in type.GetFields()) - { - if (field.IsLiteral) - { - if (this.Verb != null) - { - if (!SessionStateUtilities.MatchesAnyWildcardPattern(field.Name, matchingVerbs, false)) - { - continue; - } - } - - VerbInfo verb = new(); - verb.Verb = field.Name; - verb.AliasPrefix = VerbAliasPrefixes.GetVerbAliasPrefix(field.Name); - verb.Group = groupName; - verb.Description = VerbDescriptions.GetVerbDescription(field.Name); - WriteObject(verb); - } - } + WriteObject(verb); } } } diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index 9784cdc9ea..db6ce736bb 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Internal; using System.Management.Automation.Language; +using static System.Management.Automation.Verbs; using Dbg = System.Management.Automation.Diagnostics; namespace Microsoft.PowerShell.Commands @@ -71,6 +72,7 @@ namespace Microsoft.PowerShell.Commands /// Gets or sets the verb parameter to the cmdlet. /// [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "CmdletSet")] + [ArgumentCompleter(typeof(VerbArgumentCompleter))] public string[] Verb { get diff --git a/src/System.Management.Automation/utils/Verbs.cs b/src/System.Management.Automation/utils/Verbs.cs index e234609359..f06dd2d19e 100644 --- a/src/System.Management.Automation/utils/Verbs.cs +++ b/src/System.Management.Automation/utils/Verbs.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; +using System.Management.Automation.Language; using System.Reflection; - +using Microsoft.PowerShell.Commands; using Dbg = System.Management.Automation.Diagnostics; namespace System.Management.Automation @@ -1157,10 +1160,7 @@ namespace System.Management.Automation { static Verbs() { - Type[] verbTypes = new Type[] { typeof(VerbsCommon), typeof(VerbsCommunications), typeof(VerbsData), - typeof(VerbsDiagnostic), typeof(VerbsLifecycle), typeof(VerbsOther), typeof(VerbsSecurity) }; - - foreach (Type type in verbTypes) + foreach (Type type in VerbTypes) { foreach (FieldInfo field in type.GetFields()) { @@ -1311,6 +1311,234 @@ namespace System.Management.Automation #endif } + /// + /// Gets all verb types. + /// + /// List of all verb types. + private static Type[] VerbTypes => new Type[] { + typeof(VerbsCommon), + typeof(VerbsCommunications), + typeof(VerbsData), + typeof(VerbsDiagnostic), + typeof(VerbsLifecycle), + typeof(VerbsOther), + typeof(VerbsSecurity) + }; + + /// + /// Gets verb group display name from type. + /// + /// The verb type. + /// Verb group display name. + private static string GetVerbGroupDisplayName(Type verbType) => verbType.Name.Substring(5); + + /// + /// Filters by verbs and commands. + /// + /// The array of verbs. + /// The collection of commands. + /// List of Verbs. + private static IEnumerable FilterByVerbsAndCommands(string[] verbs, Collection commands) + { + if (commands is null || commands.Count == 0) + { + yield break; + } + + Collection verbPatterns = SessionStateUtilities.CreateWildcardsFromStrings( + verbs, + WildcardOptions.IgnoreCase); + + foreach (CmdletInfo command in commands) + { + if (SessionStateUtilities.MatchesAnyWildcardPattern( + command.Verb, + verbPatterns, + defaultValue: false)) + { + yield return command.Verb; + } + } + } + + /// + /// Filters by verbs and groups. + /// + /// The array of verbs. + /// The array of groups. + /// List of Verbs. + internal static IEnumerable FilterByVerbsAndGroups(string[] verbs, string[] groups) + { + if (groups is null || groups.Length == 0) + { + foreach (Type verbType in VerbTypes) + { + foreach (VerbInfo verb in FilterVerbsByType(verbs, verbType)) + { + yield return verb; + } + } + + yield break; + } + + foreach (Type verbType in VerbTypes) + { + if (SessionStateUtilities.CollectionContainsValue( + groups, + GetVerbGroupDisplayName(verbType), + StringComparer.OrdinalIgnoreCase)) + { + foreach (VerbInfo verb in FilterVerbsByType(verbs, verbType)) + { + yield return verb; + } + } + } + } + + /// + /// Filters verbs by type. + /// + /// The array of verbs. + /// The verb type. + /// List of Verbs. + private static IEnumerable FilterVerbsByType(string[] verbs, Type verbType) + { + if (verbs is null || verbs.Length == 0) + { + foreach (FieldInfo field in verbType.GetFields()) + { + if (field.IsLiteral) + { + yield return CreateVerbFromField(field, verbType); + } + } + + yield break; + } + + Collection verbPatterns = SessionStateUtilities.CreateWildcardsFromStrings( + verbs, + WildcardOptions.IgnoreCase); + + foreach (FieldInfo field in verbType.GetFields()) + { + if (field.IsLiteral) + { + if (SessionStateUtilities.MatchesAnyWildcardPattern( + field.Name, + verbPatterns, + defaultValue: false)) + { + yield return CreateVerbFromField(field, verbType); + } + } + } + } + + /// + /// Creates Verb info object from field info. + /// + /// The field. + /// The verb type. + /// VerbInfo object. + private static VerbInfo CreateVerbFromField(FieldInfo field, Type verbType) => new() + { + Verb = field.Name, + AliasPrefix = VerbAliasPrefixes.GetVerbAliasPrefix(field.Name), + Group = GetVerbGroupDisplayName(verbType), + Description = VerbDescriptions.GetVerbDescription(field.Name) + }; + + /// + /// Provides argument completion for Verb parameter. + /// + public class VerbArgumentCompleter : IArgumentCompleter + { + /// + /// Returns completion results for verb parameter. + /// + /// The command name. + /// The parameter name. + /// The word to complete. + /// The command AST. + /// The fake bound parameters. + /// List of Completion Results. + public IEnumerable CompleteArgument( + string commandName, + string parameterName, + string wordToComplete, + CommandAst commandAst, + IDictionary fakeBoundParameters) + { + var verbs = new string[] { wordToComplete + "*" }; + + // Completion: Get-Verb -Group -Verb + if (commandName.Equals("Get-Verb", StringComparison.OrdinalIgnoreCase) + && fakeBoundParameters.Contains("Group")) + { + string[] groups = null; + + object groupParameterValue = fakeBoundParameters["Group"]; + Type groupParameterValueType = groupParameterValue.GetType(); + + if (groupParameterValueType == typeof(string)) + { + groups = new string[] { groupParameterValue.ToString() }; + } + + else if (groupParameterValueType.IsArray + && groupParameterValueType.GetElementType() == typeof(object)) + { + groups = Array.ConvertAll((object[])groupParameterValue, group => group.ToString()); + } + + foreach (VerbInfo verb in FilterByVerbsAndGroups(verbs, groups)) + { + yield return new CompletionResult(verb.Verb); + } + + yield break; + } + + // Completion: Get-Command -Noun -Verb + else if (commandName.Equals("Get-Command", StringComparison.OrdinalIgnoreCase) + && fakeBoundParameters.Contains("Noun")) + { + using var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); + + var commandInfo = new CmdletInfo("Get-Command", typeof(GetCommandCommand)); + + ps.AddCommand(commandInfo); + ps.AddParameter("Noun", fakeBoundParameters["Noun"]); + + if (fakeBoundParameters.Contains("Module")) + { + ps.AddParameter("Module", fakeBoundParameters["Module"]); + } + + Collection commands = ps.Invoke(); + + foreach (string verb in FilterByVerbsAndCommands(verbs, commands)) + { + yield return new CompletionResult(verb); + } + + yield break; + } + + // Complete all verbs by default if above cases not completed + foreach (Type verbType in VerbTypes) + { + foreach (VerbInfo verb in FilterVerbsByType(verbs, verbType)) + { + yield return new CompletionResult(verb.Verb); + } + } + } + } + private static readonly Dictionary s_validVerbs = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary s_recommendedAlternateVerbs = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 14b4b3adf3..0b03155be3 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -744,6 +744,57 @@ ConstructorTestClass(int i, bool b) $res.CompletionMatches[0].CompletionText | Should -BeExactly '$TestVar1' } + Context 'Get-Verb & Get-Command -Verb parameter completion' { + BeforeAll { + $allVerbs = 'Add Approve Assert Backup Block Build Checkpoint Clear Close Compare Complete Compress Confirm Connect Convert ConvertFrom ConvertTo Copy Debug Deny Deploy Disable Disconnect Dismount Edit Enable Enter Exit Expand Export Find Format Get Grant Group Hide Import Initialize Install Invoke Join Limit Lock Measure Merge Mount Move New Open Optimize Out Ping Pop Protect Publish Push Read Receive Redo Register Remove Rename Repair Request Reset Resize Resolve Restart Restore Resume Revoke Save Search Select Send Set Show Skip Split Start Step Stop Submit Suspend Switch Sync Test Trace Unblock Undo Uninstall Unlock Unprotect Unpublish Unregister Update Use Wait Watch Write' + $verbsStartingWithRe = 'Read Receive Redo Register Remove Rename Repair Request Reset Resize Resolve Restart Restore Resume Revoke' + $verbsStartingWithEx = 'Exit Expand Export' + $verbsStartingWithConv = 'Convert ConvertFrom ConvertTo' + $lifeCycleVerbsStartingWithRe = 'Register Request Restart Resume' + $dataVerbsStartingwithEx = 'Expand Export' + $lifeCycleAndCommmonVerbsStartingWithRe = 'Redo Register Remove Rename Request Reset Resize Restart Resume' + $allLifeCycleAndCommonVerbs = 'Add Approve Assert Build Clear Close Complete Confirm Copy Deny Deploy Disable Enable Enter Exit Find Format Get Hide Install Invoke Join Lock Move New Open Optimize Pop Push Redo Register Remove Rename Request Reset Resize Restart Resume Search Select Set Show Skip Split Start Step Stop Submit Suspend Switch Undo Uninstall Unlock Unregister Wait Watch' + $allJsonVerbs = 'ConvertFrom ConvertTo Test' + $jsonVerbsStartingWithConv = 'ConvertFrom ConvertTo' + $allJsonAndJobVerbs = 'ConvertFrom ConvertTo Debug Get Receive Remove Start Stop Test Wait' + $jsonAndJobVerbsStartingWithSt = 'Start Stop' + $allObjectVerbs = 'Compare ForEach Group Measure New Select Sort Tee Where' + $utilityModuleObjectVerbs = 'Compare Group Measure New Select Sort Tee' + $utilityModuleObjectVerbsStartingWithS = 'Select Sort' + $coreModuleObjectVerbs = 'ForEach Where' + } + + It "Should complete Verb parameter for ''" -TestCases @( + @{ TextInput = 'Get-Verb -Verb '; ExpectedVerbs = $allVerbs } + @{ TextInput = 'Get-Verb -Group Lifecycle, Common -Verb '; ExpectedVerbs = $allLifeCycleAndCommonVerbs } + @{ TextInput = 'Get-Verb -Verb Re'; ExpectedVerbs = $verbsStartingWithRe } + @{ TextInput = 'Get-Verb -Group Lifecycle -Verb Re'; ExpectedVerbs = $lifeCycleVerbsStartingWithRe } + @{ TextInput = 'Get-Verb -Group Lifecycle, Common -Verb Re'; ExpectedVerbs = $lifeCycleAndCommmonVerbsStartingWithRe } + @{ TextInput = 'Get-Verb -Verb Ex'; ExpectedVerbs = $verbsStartingWithEx } + @{ TextInput = 'Get-Verb -Group Data -Verb Ex'; ExpectedVerbs = $dataVerbsStartingwithEx } + @{ TextInput = 'Get-Verb -Group NonExistentGroup -Verb '; ExpectedVerbs = '' } + @{ TextInput = 'Get-Verb -Verb Conv'; ExpectedVerbs = $verbsStartingWithConv } + @{ TextInput = 'Get-Command -Verb '; ExpectedVerbs = $allVerbs } + @{ TextInput = 'Get-Command -Verb Re'; ExpectedVerbs = $verbsStartingWithRe } + @{ TextInput = 'Get-Command -Verb Ex'; ExpectedVerbs = $verbsStartingWithEx } + @{ TextInput = 'Get-Command -Verb Conv'; ExpectedVerbs = $verbsStartingWithConv } + @{ TextInput = 'Get-Command -Noun Json -Verb '; ExpectedVerbs = $allJsonVerbs } + @{ TextInput = 'Get-Command -Noun Json -Verb Conv'; ExpectedVerbs = $jsonVerbsStartingWithConv } + @{ TextInput = 'Get-Command -Noun Json, Job -Verb '; ExpectedVerbs = $allJsonAndJobVerbs } + @{ TextInput = 'Get-Command -Noun Json, Job -Verb St'; ExpectedVerbs = $jsonAndJobVerbsStartingWithSt } + @{ TextInput = 'Get-Command -Noun NonExistentNoun -Verb '; ExpectedVerbs = '' } + @{ TextInput = 'Get-Command -Noun Object -Module Microsoft.PowerShell.Utility,Microsoft.PowerShell.Core -Verb '; ExpectedVerbs = $allObjectVerbs } + @{ TextInput = 'Get-Command -Noun Object -Module Microsoft.PowerShell.Utility -Verb '; ExpectedVerbs = $utilityModuleObjectVerbs } + @{ TextInput = 'Get-Command -Noun Object -Module Microsoft.PowerShell.Utility -Verb S'; ExpectedVerbs = $utilityModuleObjectVerbsStartingWithS } + @{ TextInput = 'Get-Command -Noun Object -Module Microsoft.PowerShell.Core -Verb '; ExpectedVerbs = $coreModuleObjectVerbs } + ) { + param($TextInput, $ExpectedVerbs) + $res = TabExpansion2 -inputScript $TextInput -cursorColumn $TextInput.Length + $completionText = $res.CompletionMatches.CompletionText | Sort-Object + $completionText -join ' ' | Should -BeExactly $ExpectedVerbs + } + } + Context 'StrictMode Version parameter completion' { BeforeAll { $allStrictModeVersions = '1.0 2.0 3.0 Latest'