diff --git a/src/System.Management.Automation/engine/CommandCompletion/ExtensibleCompletion.cs b/src/System.Management.Automation/engine/CommandCompletion/ExtensibleCompletion.cs index c63a8e7f92d..57bbeb1db49 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/ExtensibleCompletion.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/ExtensibleCompletion.cs @@ -280,6 +280,315 @@ static void SetKeyValue(Dictionary table, string key, Scrip } } + /// + /// Specifies the type of argument completer. + /// + public enum ArgumentCompleterType + { + /// + /// A completer for PowerShell command parameters. + /// + PowerShell, + + /// + /// A completer for native command arguments. + /// + Native, + + /// + /// A fallback completer for native commands that don't have a specific completer. + /// + NativeFallback, + } + + /// + /// Represents information about a registered argument completer. + /// + public sealed class ArgumentCompleterInfo + { + /// + /// Gets the command name associated with this completer. + /// For PowerShell completers without a command, this may be null. + /// For native fallback completers, this is null. + /// + public string CommandName { get; } + + /// + /// Gets the parameter name for PowerShell completers. + /// Null for native command completers. + /// + public string ParameterName { get; } + + /// + /// Gets the script block that provides completions. + /// + public ScriptBlock ScriptBlock { get; } + + /// + /// Gets the type of this argument completer. + /// + public ArgumentCompleterType Type { get; } + + internal ArgumentCompleterInfo(string commandName, string parameterName, ScriptBlock scriptBlock, ArgumentCompleterType type) + { + CommandName = commandName; + ParameterName = parameterName; + ScriptBlock = scriptBlock; + Type = type; + } + } + + /// + /// Gets registered argument completers. + /// + [Cmdlet(VerbsCommon.Get, "ArgumentCompleter", HelpUri = "https://go.microsoft.com/fwlink/?LinkId=528576")] + [OutputType(typeof(ArgumentCompleterInfo))] + public class GetArgumentCompleterCommand : PSCmdlet + { + private const string PowerShellSetName = "PowerShellSet"; + private const string NativeSetName = "NativeSet"; + + /// + /// Gets or sets the command name to filter completers. + /// + [Parameter(Position = 0, ParameterSetName = PowerShellSetName)] + [Parameter(Position = 0, ParameterSetName = NativeSetName)] + [SupportsWildcards] + public string[] CommandName { get; set; } + + /// + /// Gets or sets the parameter name to filter PowerShell completers. + /// + [Parameter(Position = 1, ParameterSetName = PowerShellSetName)] + [SupportsWildcards] + public string[] ParameterName { get; set; } + + /// + /// If specified, returns native command completers. + /// + [Parameter(ParameterSetName = NativeSetName)] + public SwitchParameter Native { get; set; } + + /// + /// EndProcessing implementation. + /// + protected override void EndProcessing() + { + var commandPatterns = CreateWildcardPatterns(CommandName); + var parameterPatterns = CreateWildcardPatterns(ParameterName); + + if (Native.IsPresent) + { + // Return native command completers + var nativeCompleters = Context.NativeArgumentCompleters; + if (nativeCompleters != null) + { + foreach (var kvp in nativeCompleters) + { + if (kvp.Key == RegisterArgumentCompleterCommand.FallbackCompleterKey) + { + // Only include fallback if no CommandName filter or if filtering explicitly + if (commandPatterns == null) + { + WriteObject(new ArgumentCompleterInfo(null, null, kvp.Value, ArgumentCompleterType.NativeFallback)); + } + } + else + { + if (MatchesWildcardPatterns(kvp.Key, commandPatterns)) + { + WriteObject(new ArgumentCompleterInfo(kvp.Key, null, kvp.Value, ArgumentCompleterType.Native)); + } + } + } + } + } + else + { + // Return PowerShell command completers + var customCompleters = Context.CustomArgumentCompleters; + if (customCompleters != null) + { + foreach (var kvp in customCompleters) + { + // Key format is either "ParameterName" or "CommandName:ParameterName" + var colonIndex = kvp.Key.IndexOf(':'); + string cmdName = null; + string paramName; + + if (colonIndex >= 0) + { + cmdName = kvp.Key.Substring(0, colonIndex); + paramName = kvp.Key.Substring(colonIndex + 1); + } + else + { + paramName = kvp.Key; + } + + // Apply filters + if (!MatchesWildcardPatterns(cmdName, commandPatterns)) + { + continue; + } + + if (!MatchesWildcardPatterns(paramName, parameterPatterns)) + { + continue; + } + + WriteObject(new ArgumentCompleterInfo(cmdName, paramName, kvp.Value, ArgumentCompleterType.PowerShell)); + } + } + } + } + + private static WildcardPattern[] CreateWildcardPatterns(string[] values) + { + if (values == null || values.Length == 0) + { + return null; + } + + var patterns = new WildcardPattern[values.Length]; + for (int i = 0; i < values.Length; i++) + { + patterns[i] = WildcardPattern.Get(values[i], WildcardOptions.IgnoreCase); + } + + return patterns; + } + + private static bool MatchesWildcardPatterns(string value, WildcardPattern[] patterns) + { + if (patterns == null) + { + return true; + } + + if (value == null) + { + // For null values, only match if one of the patterns is "*" or null + foreach (var pattern in patterns) + { + if (pattern.IsMatch(string.Empty)) + { + return true; + } + } + + return false; + } + + foreach (var pattern in patterns) + { + if (pattern.IsMatch(value)) + { + return true; + } + } + + return false; + } + } + + /// + /// Unregisters argument completers. + /// + [Cmdlet(VerbsLifecycle.Unregister, "ArgumentCompleter", HelpUri = "https://go.microsoft.com/fwlink/?LinkId=528576")] + public class UnregisterArgumentCompleterCommand : PSCmdlet + { + private const string PowerShellSetName = "PowerShellSet"; + private const string NativeCommandSetName = "NativeCommandSet"; + private const string NativeFallbackSetName = "NativeFallbackSet"; + + /// + /// Gets or sets the command names for which to unregister the argument completer. + /// + [Parameter(ParameterSetName = NativeCommandSetName, Mandatory = true)] + [Parameter(ParameterSetName = PowerShellSetName)] + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + public string[] CommandName { get; set; } + + /// + /// Gets or sets the parameter name for which to unregister the argument completer. + /// + [Parameter(ParameterSetName = PowerShellSetName, Mandatory = true)] + public string ParameterName { get; set; } + + /// + /// Indicates the argument completer is for native commands. + /// + [Parameter(ParameterSetName = NativeCommandSetName)] + public SwitchParameter Native { get; set; } + + /// + /// Indicates to unregister the fallback completer for native commands. + /// + [Parameter(ParameterSetName = NativeFallbackSetName, Mandatory = true)] + public SwitchParameter NativeFallback { get; set; } + + /// + /// EndProcessing implementation. + /// + protected override void EndProcessing() + { + if (ParameterSetName is NativeFallbackSetName) + { + var nativeCompleters = Context.NativeArgumentCompleters; + if (nativeCompleters != null) + { + nativeCompleters.Remove(RegisterArgumentCompleterCommand.FallbackCompleterKey); + } + } + else if (ParameterSetName is NativeCommandSetName) + { + var nativeCompleters = Context.NativeArgumentCompleters; + if (nativeCompleters != null) + { + foreach (string command in CommandName) + { + var key = command?.Trim(); + if (string.IsNullOrEmpty(key)) + { + continue; + } + + nativeCompleters.Remove(key); + } + } + } + else if (ParameterSetName is PowerShellSetName) + { + var customCompleters = Context.CustomArgumentCompleters; + if (customCompleters != null) + { + string paramName = ParameterName.Trim(); + if (paramName.Length is 0) + { + return; + } + + if (CommandName is null || CommandName.Length is 0) + { + customCompleters.Remove(paramName); + return; + } + + foreach (string command in CommandName) + { + var key = command?.Trim(); + key = string.IsNullOrEmpty(key) + ? paramName + : $"{key}:{paramName}"; + + customCompleters.Remove(key); + } + } + } + } + } + /// /// This attribute is used to specify an argument completions for a parameter of a cmdlet or function /// based on string array. @@ -331,4 +640,4 @@ public IEnumerable CompleteArgument(string commandName, string } } } -} +} \ No newline at end of file diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index 62308282d17..094e450371c 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -5470,6 +5470,7 @@ private static void InitializeCoreCmdletsAndProviders( { "Exit-PSSession", new SessionStateCmdletEntry("Exit-PSSession", typeof(ExitPSSessionCommand), helpFile) }, { "Export-ModuleMember", new SessionStateCmdletEntry("Export-ModuleMember", typeof(ExportModuleMemberCommand), helpFile) }, { "ForEach-Object", new SessionStateCmdletEntry("ForEach-Object", typeof(ForEachObjectCommand), helpFile) }, + { "Get-ArgumentCompleter", new SessionStateCmdletEntry("Get-ArgumentCompleter", typeof(GetArgumentCompleterCommand), helpFile) }, { "Get-Command", new SessionStateCmdletEntry("Get-Command", typeof(GetCommandCommand), helpFile) }, { "Get-ExperimentalFeature", new SessionStateCmdletEntry("Get-ExperimentalFeature", typeof(GetExperimentalFeatureCommand), helpFile) }, { "Get-Help", new SessionStateCmdletEntry("Get-Help", typeof(GetHelpCommand), helpFile) }, @@ -5503,6 +5504,7 @@ private static void InitializeCoreCmdletsAndProviders( { "Start-Job", new SessionStateCmdletEntry("Start-Job", typeof(StartJobCommand), helpFile) }, { "Stop-Job", new SessionStateCmdletEntry("Stop-Job", typeof(StopJobCommand), helpFile) }, { "Test-ModuleManifest", new SessionStateCmdletEntry("Test-ModuleManifest", typeof(TestModuleManifestCommand), helpFile) }, + { "Unregister-ArgumentCompleter", new SessionStateCmdletEntry("Unregister-ArgumentCompleter", typeof(UnregisterArgumentCompleterCommand), helpFile) }, { "Update-Help", new SessionStateCmdletEntry("Update-Help", typeof(UpdateHelpCommand), helpFile) }, { "Wait-Job", new SessionStateCmdletEntry("Wait-Job", typeof(WaitJobCommand), helpFile) }, { "Where-Object", new SessionStateCmdletEntry("Where-Object", typeof(WhereObjectCommand), helpFile) }, diff --git a/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 b/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 index 3514f389718..b5be6b5d387 100644 --- a/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 +++ b/test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1 @@ -522,3 +522,123 @@ Describe "ArgumentCompletionsAttribute tests" -Tags "CI" { { TestArgumentCompletionsAttribute -Param1 unExpectedValue } | Should -Not -Throw } } + + +Describe "Get-ArgumentCompleter cmdlet" -Tags "CI" { + BeforeAll { + # Register test completers + Register-ArgumentCompleter -CommandName TestGetCmd -ParameterName TestParam -ScriptBlock { "test1" } + Register-ArgumentCompleter -ParameterName GlobalTestParam -ScriptBlock { "global" } + Register-ArgumentCompleter -Native -CommandName testnative -ScriptBlock { "native1" } + } + + AfterAll { + # Clean up + Unregister-ArgumentCompleter -CommandName TestGetCmd -ParameterName TestParam + Unregister-ArgumentCompleter -ParameterName GlobalTestParam + Unregister-ArgumentCompleter -Native -CommandName testnative + } + + It "Returns PowerShell completers by default" { + $results = Get-ArgumentCompleter + $results | Should -Not -BeNullOrEmpty + $results | ForEach-Object { $_.Type | Should -Be 'PowerShell' } + } + + It "Returns native completers with -Native switch" { + $results = Get-ArgumentCompleter -Native + $results | Should -Not -BeNullOrEmpty + $results | ForEach-Object { $_.Type | Should -BeIn @('Native', 'NativeFallback') } + } + + It "Filters by CommandName" { + $results = Get-ArgumentCompleter -CommandName TestGetCmd + $results | Should -Not -BeNullOrEmpty + $results.CommandName | Should -Contain 'TestGetCmd' + } + + It "Filters by ParameterName" { + $results = Get-ArgumentCompleter -ParameterName TestParam + $results | Should -Not -BeNullOrEmpty + $results.ParameterName | Should -Contain 'TestParam' + } + + It "Supports wildcards in CommandName" { + $results = Get-ArgumentCompleter -CommandName "TestGet*" + $results | Should -Not -BeNullOrEmpty + $results.CommandName | Should -Contain 'TestGetCmd' + } + + It "Supports wildcards in ParameterName" { + $results = Get-ArgumentCompleter -ParameterName "*TestParam" + $results | Should -Not -BeNullOrEmpty + } + + It "Returns ArgumentCompleterInfo objects with correct properties" { + $results = Get-ArgumentCompleter -CommandName TestGetCmd + $result = $results | Where-Object { $_.CommandName -eq 'TestGetCmd' } + $result | Should -Not -BeNullOrEmpty + $result.CommandName | Should -Be 'TestGetCmd' + $result.ParameterName | Should -Be 'TestParam' + $result.ScriptBlock | Should -Not -BeNullOrEmpty + $result.Type | Should -Be 'PowerShell' + } + + It "Returns native completer with correct Type" { + $results = Get-ArgumentCompleter -Native -CommandName testnative + $result = $results | Where-Object { $_.CommandName -eq 'testnative' } + $result | Should -Not -BeNullOrEmpty + $result.Type | Should -Be 'Native' + $result.ParameterName | Should -BeNullOrEmpty + } +} + +Describe "Unregister-ArgumentCompleter cmdlet" -Tags "CI" { + It "Removes PowerShell completer by command and parameter" { + Register-ArgumentCompleter -CommandName UnregisterTest -ParameterName TestParam -ScriptBlock { "test" } + $before = Get-ArgumentCompleter -CommandName UnregisterTest + $before | Should -Not -BeNullOrEmpty + + Unregister-ArgumentCompleter -CommandName UnregisterTest -ParameterName TestParam + + $after = Get-ArgumentCompleter -CommandName UnregisterTest -ParameterName TestParam + $after | Should -BeNullOrEmpty + } + + It "Removes global parameter completer" { + Register-ArgumentCompleter -ParameterName UnregisterGlobalParam -ScriptBlock { "global" } + $before = Get-ArgumentCompleter -ParameterName UnregisterGlobalParam + $before | Should -Not -BeNullOrEmpty + + Unregister-ArgumentCompleter -ParameterName UnregisterGlobalParam + + $after = Get-ArgumentCompleter -ParameterName UnregisterGlobalParam + $after | Should -BeNullOrEmpty + } + + It "Removes native command completer" { + Register-ArgumentCompleter -Native -CommandName unregisternative -ScriptBlock { "native" } + $before = Get-ArgumentCompleter -Native -CommandName unregisternative + $before | Should -Not -BeNullOrEmpty + + Unregister-ArgumentCompleter -Native -CommandName unregisternative + + $after = Get-ArgumentCompleter -Native -CommandName unregisternative + $after | Should -BeNullOrEmpty + } + + It "Removes native fallback completer" { + Register-ArgumentCompleter -NativeFallback -ScriptBlock { "fallback" } + $before = Get-ArgumentCompleter -Native | Where-Object { $_.Type -eq 'NativeFallback' } + $before | Should -Not -BeNullOrEmpty + + Unregister-ArgumentCompleter -NativeFallback + + $after = Get-ArgumentCompleter -Native | Where-Object { $_.Type -eq 'NativeFallback' } + $after | Should -BeNullOrEmpty + } + + It "Does not error when removing non-existent completer" { + { Unregister-ArgumentCompleter -CommandName NonExistent -ParameterName NonExistent } | Should -Not -Throw + } +}