Add -Verb argument completer for Start-Process (#20415)

This commit is contained in:
Armaan Mcleod
2023-12-19 21:18:04 +05:00
committed by GitHub
parent 9383f554a2
commit 2ad03151f8
2 changed files with 132 additions and 0 deletions
@@ -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
/// </remarks>
[Parameter(ParameterSetName = "UseShellExecute")]
[ValidateNotNullOrEmpty]
[ArgumentCompleter(typeof(VerbArgumentCompleter))]
public string Verb { get; set; }
/// <summary>
@@ -2656,6 +2659,95 @@ namespace Microsoft.PowerShell.Commands
#endregion
}
/// <summary>
/// Provides argument completion for Verb parameter.
/// </summary>
public class VerbArgumentCompleter : IArgumentCompleter
{
/// <summary>
/// Returns completion results for verb parameter.
/// </summary>
/// <param name="commandName">The command name.</param>
/// <param name="parameterName">The parameter name.</param>
/// <param name="wordToComplete">The word to complete.</param>
/// <param name="commandAst">The command AST.</param>
/// <param name="fakeBoundParameters">The fake bound parameters.</param>
/// <returns>List of Completion Results.</returns>
public IEnumerable<CompletionResult> 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 <path> -Verb <wordToComplete>
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<CommandInfo> commands = ps.Invoke<CommandInfo>();
// 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);
}
}
}
}
/// <summary>
/// Completes file verbs.
/// </summary>
/// <param name="filePath">The file path to get verbs.</param>
/// <param name="wordToComplete">The word to complete.</param>
/// <returns>List of file verbs to complete.</returns>
private static IEnumerable<string> 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
/// <summary>
/// ProcessCollection is a helper class used by Start-Process -Wait cmdlet to monitor the
@@ -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 '<TextInput>'" -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'