From 2ad03151f8c48c3a144d9af51ed569eff2abe4ea Mon Sep 17 00:00:00 2001 From: Armaan Mcleod Date: Wed, 20 Dec 2023 03:18:04 +1100 Subject: [PATCH] Add `-Verb` argument completer for `Start-Process` (#20415) --- .../commands/management/Process.cs | 92 +++++++++++++++++++ .../TabCompletion/TabCompletion.Tests.ps1 | 40 ++++++++ 2 files changed, 132 insertions(+) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs index 2dc487d0e3..1d877ed5a2 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; // Win32Exception using System.Diagnostics; // Process class @@ -11,6 +12,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Management.Automation; using System.Management.Automation.Internal; +using System.Management.Automation.Language; using System.Net; using System.Runtime.InteropServices; using System.Runtime.Serialization; @@ -1800,6 +1802,7 @@ namespace Microsoft.PowerShell.Commands /// [Parameter(ParameterSetName = "UseShellExecute")] [ValidateNotNullOrEmpty] + [ArgumentCompleter(typeof(VerbArgumentCompleter))] public string Verb { get; set; } /// @@ -2656,6 +2659,95 @@ namespace Microsoft.PowerShell.Commands #endregion } + /// + /// 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) + { + // -Verb is not supported on non-Windows platforms as well as Windows headless SKUs + if (!Platform.IsWindowsDesktop) + { + yield break; + } + + // Completion: Start-Process -FilePath -Verb + if (commandName.Equals("Start-Process", StringComparison.OrdinalIgnoreCase) + && fakeBoundParameters.Contains("FilePath")) + { + string filePath = fakeBoundParameters["FilePath"].ToString(); + + // Complete file verbs if extension exists + if (Path.HasExtension(filePath)) + { + foreach (string verb in CompleteFileVerbs(filePath, wordToComplete)) + { + yield return new CompletionResult(verb); + } + + yield break; + } + + // Otherwise check if command is an Application to resolve executable full path with extension + // e.g if powershell was given, resolve to powershell.exe to get verbs + using var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + + var commandInfo = new CmdletInfo("Get-Command", typeof(GetCommandCommand)); + + ps.AddCommand(commandInfo); + ps.AddParameter("Name", filePath); + ps.AddParameter("CommandType", CommandTypes.Application); + + Collection commands = ps.Invoke(); + + // Start-Process & Get-Command select first found application based on PATHEXT environment variable + if (commands.Count >= 1) + { + foreach (string verb in CompleteFileVerbs(commands[0].Source, wordToComplete)) + { + yield return new CompletionResult(verb); + } + } + } + } + + /// + /// Completes file verbs. + /// + /// The file path to get verbs. + /// The word to complete. + /// List of file verbs to complete. + private static IEnumerable CompleteFileVerbs(string filePath, string wordToComplete) + { + var verbPattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase); + + string[] verbs = new ProcessStartInfo(filePath).Verbs; + + foreach (string verb in verbs) + { + if (verbPattern.IsMatch(verb)) + { + yield return verb; + } + } + } + } + #if !UNIX /// /// ProcessCollection is a helper class used by Start-Process -Wait cmdlet to monitor the diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 7e72b3fff0..27b6135cf5 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -744,6 +744,46 @@ ConstructorTestClass(int i, bool b) $res.CompletionMatches[0].CompletionText | Should -BeExactly '$TestVar1' } + Context 'Start-Process -Verb parameter completion' { + BeforeAll { + function GetProcessInfoVerbs($path) { + (New-Object -TypeName System.Diagnostics.ProcessStartInfo -ArgumentList $path).Verbs + } + + $cmdPath = Join-Path -Path $TestDrive -ChildPath 'test.cmd' + $cmdVerbs = GetProcessInfoVerbs $cmdPath + $exePath = Join-Path -Path $TestDrive -ChildPath 'test.exe' + $exeVerbs = GetProcessInfoVerbs $exePath + $exeVerbsStartingWithRun = $exeVerbs | Where-Object { $_ -like 'run*' } + $powerShellExeWithNoExtension = 'powershell' + $txtPath = Join-Path -Path $TestDrive -ChildPath 'test.txt' + $txtVerbs = GetProcessInfoVerbs $txtPath + $wavPath = Join-Path -Path $TestDrive -ChildPath 'test.wav' + $wavVerbs = GetProcessInfoVerbs $wavPath + $docxPath = Join-Path -Path $TestDrive -ChildPath 'test.docx' + $docxVerbs = GetProcessInfoVerbs $docxPath + $fileWithNoExtensionPath = Join-Path -Path $TestDrive -ChildPath 'test' + $fileWithNoExtensionVerbs = GetProcessInfoVerbs $fileWithNoExtensionPath + } + + It "Should complete Verb parameter for ''" -Skip:(!([System.Management.Automation.Platform]::IsWindowsDesktop)) -TestCases @( + @{ TextInput = 'Start-Process -Verb '; ExpectedVerbs = '' } + @{ TextInput = "Start-Process -FilePath $cmdPath -Verb "; ExpectedVerbs = $cmdVerbs -join ' ' } + @{ TextInput = "Start-Process -FilePath $exePath -Verb "; ExpectedVerbs = $exeVerbs -join ' ' } + @{ TextInput = "Start-Process -FilePath $exePath -Verb run"; ExpectedVerbs = $exeVerbsStartingWithRun -join ' ' } + @{ TextInput = "Start-Process -FilePath $powerShellExeWithNoExtension -Verb "; ExpectedVerbs = $exeVerbs -join ' ' } + @{ TextInput = "Start-Process -FilePath $txtPath -Verb "; ExpectedVerbs = $txtVerbs -join ' ' } + @{ TextInput = "Start-Process -FilePath $wavPath -Verb "; ExpectedVerbs = $wavVerbs -join ' ' } + @{ TextInput = "Start-Process -FilePath $docxPath -Verb "; ExpectedVerbs = $docxVerbs -join ' ' } + @{ TextInput = "Start-Process -FilePath $fileWithNoExtensionPath -Verb "; ExpectedVerbs = $fileWithNoExtensionVerbs -join ' ' } + ) { + param($TextInput, $ExpectedVerbs) + $res = TabExpansion2 -inputScript $TextInput -cursorColumn $TextInput.Length + $completionText = $res.CompletionMatches.CompletionText | Sort-Object + $completionText -join ' ' | Should -BeExactly $ExpectedVerbs + } + } + Context 'Scope parameter completion' { BeforeAll { $allScopes = 'Global Local Script'