Skip to content

Commit 93dc591

Browse files
iSazonovdaxian-dbw
authored andcommitted
Add 'ArgumentCompletionsAttribute' to support more argument completion scenarios (PowerShell#4835)
- Add 'ArgumentCompletionsAttribute' to support argument completion for parameters that cannot have a ValidateSetAttribute. - Use 'ArgumentCompletionsAttribute' for the '-Format' parameter of 'Get-Date' to enable useful argument compeltions.
1 parent 84ac521 commit 93dc591

File tree

7 files changed

+203
-22
lines changed

7 files changed

+203
-22
lines changed

src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetDateCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ public int Millisecond
206206
/// Unix format string
207207
/// </summary>
208208
[Parameter(ParameterSetName = "net")]
209+
[ArgumentCompletions("FileDate", "FileDateUniversal", "FileDateTime", "FileDateTimeUniversal")]
209210
public string Format { get; set; }
210211

211212
#endregion

src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2002,6 +2002,20 @@ private static void NativeCommandArgumentCompletion(
20022002
{
20032003
}
20042004
}
2005+
2006+
var argumentCompletionsAttribute = parameter.CompiledAttributes.OfType<ArgumentCompletionsAttribute>().FirstOrDefault();
2007+
if (argumentCompletionsAttribute != null)
2008+
{
2009+
var customResults = argumentCompletionsAttribute.CompleteArgument(commandName, parameterName,
2010+
context.WordToComplete, commandAst, GetBoundArgumentsAsHashtable(context));
2011+
if (customResults != null)
2012+
{
2013+
result.AddRange(customResults);
2014+
result.Add(CompletionResult.Null);
2015+
return;
2016+
}
2017+
}
2018+
20052019
switch (commandName)
20062020
{
20072021
case "Get-Command":

src/System.Management.Automation/engine/CommandCompletion/ExtensibleCompletion.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,56 @@ protected override void EndProcessing()
158158
}
159159
}
160160
}
161+
162+
/// <summary>
163+
/// This attribute is used to specify an argument completions for a parameter of a cmdlet or function
164+
/// based on string array.
165+
/// <example>
166+
/// [Parameter()]
167+
/// [ArgumentCompletions("Option1","Option2","Option3")]
168+
/// public string Noun { get; set; }
169+
/// </example>
170+
/// </summary>
171+
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
172+
public class ArgumentCompletionsAttribute : Attribute
173+
{
174+
private string[] _completions;
175+
176+
/// <summary>
177+
/// Initializes a new instance of the ArgumentCompletionsAttribute class
178+
/// </summary>
179+
/// <param name="completions">list of complete values</param>
180+
/// <exception cref="ArgumentNullException">for null arguments</exception>
181+
/// <exception cref="ArgumentOutOfRangeException">for invalid arguments</exception>
182+
public ArgumentCompletionsAttribute(params string[] completions)
183+
{
184+
if (completions == null)
185+
{
186+
throw PSTraceSource.NewArgumentNullException("completions");
187+
}
188+
189+
if (completions.Length == 0)
190+
{
191+
throw PSTraceSource.NewArgumentOutOfRangeException("completions", completions);
192+
}
193+
194+
_completions = completions;
195+
}
196+
197+
/// <summary>
198+
/// The function returns completions for arguments.
199+
/// </summary>
200+
public IEnumerable<CompletionResult> CompleteArgument(string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters)
201+
{
202+
var wordToCompletePattern = WildcardPattern.Get(string.IsNullOrWhiteSpace(wordToComplete) ? "*" : wordToComplete + "*", WildcardOptions.IgnoreCase);
203+
204+
foreach (var str in _completions)
205+
{
206+
if (wordToCompletePattern.IsMatch(str))
207+
{
208+
yield return new CompletionResult(str, str, CompletionResultType.ParameterValue, str);
209+
}
210+
}
211+
}
212+
}
161213
}

src/System.Management.Automation/engine/parser/TypeResolver.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,7 @@ internal static class CoreTypes
736736
{ typeof(AllowEmptyStringAttribute), new[] { "AllowEmptyString" } },
737737
{ typeof(AllowNullAttribute), new[] { "AllowNull" } },
738738
{ typeof(ArgumentCompleterAttribute), new[] { "ArgumentCompleter" } },
739+
{ typeof(ArgumentCompletionsAttribute), new[] { "ArgumentCompletions" } },
739740
{ typeof(Array), new[] { "array" } },
740741
{ typeof(bool), new[] { "bool" } },
741742
{ typeof(byte), new[] { "byte" } },

test/powershell/Language/Parser/ExtensibleCompletion.Tests.ps1

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,97 @@ Describe "Additional type name completion tests" -Tags "CI" {
355355
TestInput = 'Get-Command -ParameterType System.Collections.Generic.Dic'
356356
} | Get-CompletionTestCaseData | Test-Completions
357357
}
358+
359+
Describe "ArgumentCompletionsAttribute tests" -Tags "CI" {
360+
361+
BeforeAll {
362+
function TestArgumentCompletionsAttribute
363+
{
364+
param(
365+
[ArgumentCompletions("value1", "value2", "value3")]
366+
$Alpha,
367+
$Beta
368+
)
369+
}
370+
371+
function TestArgumentCompletionsAttribute1
372+
{
373+
param(
374+
[ArgumentCompletionsAttribute("value1", "value2", "value3")]
375+
$Alpha,
376+
$Beta
377+
)
378+
}
379+
380+
$cmdletSrc=@'
381+
using System;
382+
using System.Management.Automation;
383+
using System.Collections.Generic;
384+
385+
namespace Test.A {
386+
387+
[Cmdlet(VerbsCommon.Get, "ArgumentCompletions")]
388+
public class TestArgumentCompletionsAttributeCommand : PSCmdlet
389+
{
390+
[Parameter]
391+
[ArgumentCompletions("value1", "value2", "value3")]
392+
public string Param1;
393+
394+
protected override void EndProcessing()
395+
{
396+
WriteObject(Param1);
397+
}
398+
}
399+
400+
[Cmdlet(VerbsCommon.Get, "ArgumentCompletions1")]
401+
public class TestArgumentCompletionsAttributeCommand1 : PSCmdlet
402+
{
403+
[Parameter]
404+
[ArgumentCompletionsAttribute("value1", "value2", "value3")]
405+
public string Param1;
406+
407+
protected override void EndProcessing()
408+
{
409+
WriteObject(Param1);
410+
}
411+
}
412+
}
413+
'@
414+
$cls = Add-Type -TypeDefinition $cmdletSrc -PassThru | Select-Object -First 1
415+
$testModule = Import-Module $cls.Assembly -PassThru
416+
417+
$testCasesScript = @(
418+
@{ attributeName = "ArgumentCompletions" ; cmdletName = "TestArgumentCompletionsAttribute" },
419+
@{ attributeName = "ArgumentCompletionsAttribute"; cmdletName = "TestArgumentCompletionsAttribute1" }
420+
)
421+
422+
$testCasesCSharp = @(
423+
@{ attributeName = "ArgumentCompletions" ; cmdletName = "Get-ArgumentCompletions" },
424+
@{ attributeName = "ArgumentCompletionsAttribute"; cmdletName = "Get-ArgumentCompletions1" }
425+
)
426+
}
427+
428+
AfterAll {
429+
Remove-Module -ModuleInfo $testModule
430+
}
431+
432+
It "<attributeName> works in script" -TestCases $testCasesScript {
433+
param($attributeName, $cmdletName)
434+
435+
$line = "$cmdletName -Alpha val"
436+
$res = TaBexpansion2 -inputScript $line -cursorColumn $line.Length
437+
$res.CompletionMatches.Count | Should Be 3
438+
$res.CompletionMatches.CompletionText -join " " | Should Be "value1 value2 value3"
439+
{ TestArgumentCompletionsAttribute -Alpha unExpectedValue } | Should Not Throw
440+
}
441+
442+
It "<attributeName> works in C#" -TestCases $testCasesCSharp {
443+
param($attributeName, $cmdletName)
444+
445+
$line = "$cmdletName -Param1 val"
446+
$res = TaBexpansion2 -inputScript $line -cursorColumn $line.Length
447+
$res.CompletionMatches.Count | Should Be 3
448+
$res.CompletionMatches.CompletionText -join " " | Should Be "value1 value2 value3"
449+
{ TestArgumentCompletionsAttribute -Param1 unExpectedValue } | Should Not Throw
450+
}
451+
}

test/powershell/Language/Parser/TypeAccelerator.Tests.ps1

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ Describe "Type accelerators" -Tags "CI" {
2828
Accelerator = 'ArgumentCompleter'
2929
Type = [System.Management.Automation.ArgumentCompleterAttribute]
3030
}
31+
@{
32+
Accelerator = 'ArgumentCompletions'
33+
Type = [System.Management.Automation.ArgumentCompletionsAttribute]
34+
}
3135
@{
3236
Accelerator = 'array'
3337
Type = [System.Array]
@@ -368,7 +372,7 @@ Describe "Type accelerators" -Tags "CI" {
368372

369373
if ( $IsCoreCLR )
370374
{
371-
$totalAccelerators = 89
375+
$totalAccelerators = 90
372376
}
373377
else
374378
{

test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Date.Tests.ps1

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,34 +81,49 @@ Describe "Get-Date DRT Unit Tests" -Tags "CI" {
8181

8282

8383
Describe "Get-Date" -Tags "CI" {
84+
It "-Format FileDate works" {
85+
Get-date -Date 0030-01-01T01:02:03.0004 -Format FileDate | Should Be "00300101"
86+
}
87+
88+
It "-Format FileDateTime works" {
89+
Get-date -Date 0030-01-01T01:02:03.0004 -Format FileDateTime | Should Be "00300101T0102030004"
90+
}
91+
92+
It "-Format FileDateTimeUniversal works" {
93+
Get-date -Date 0030-01-01T01:02:03.0004z -Format FileDateTimeUniversal | Should Be "00300101T0102030004Z"
94+
}
95+
96+
It "-Format FileDateTimeUniversal works" {
97+
Get-date -Date 0030-01-01T01:02:03.0004z -Format FileDateUniversal | Should Be "00300101Z"
98+
}
99+
84100
It "Should have colons when ToString method is used" {
85-
(Get-Date).ToString().Contains(":") | Should be $true
86-
(Get-Date -DisplayHint Time).ToString().Contains(":") | Should be $true
87-
(Get-Date -DisplayHint Date).ToString().Contains(":") | Should be $true
101+
(Get-Date).ToString().Contains(":") | Should be $true
102+
(Get-Date -DisplayHint Time).ToString().Contains(":") | Should be $true
103+
(Get-Date -DisplayHint Date).ToString().Contains(":") | Should be $true
88104
}
89105

90106
It "Should be able to use the format flag" {
91-
# You would think that one could use simple loops here, but apparently powershell in Windows returns different values in loops
92-
93-
(Get-Date -Format d).Contains("/") | Should be $true
94-
(Get-Date -Format D).Contains(",") | Should be $true
95-
(Get-Date -Format f).Contains(",") -and (Get-Date -Format f).Contains(":") | Should be $true
96-
(Get-Date -Format F).Contains(",") -and (Get-Date -Format F).Contains(":") | Should be $true
97-
(Get-Date -Format g).Contains("/") -and (Get-Date -Format g).Contains(":") | Should be $true
98-
(Get-Date -Format G).Contains("/") -and (Get-Date -Format G).Contains(":") | Should be $true
99-
(Get-Date -Format m).Contains(",") -or `
100-
(Get-Date -Format m).Contains(":") -or `
101-
(Get-Date -Format m).Contains("/") | Should be $false
107+
# You would think that one could use simple loops here, but apparently powershell in Windows returns different values in loops
108+
109+
(Get-Date -Format d).Contains("/") | Should be $true
110+
(Get-Date -Format D).Contains(",") | Should be $true
111+
(Get-Date -Format f).Contains(",") -and (Get-Date -Format f).Contains(":") | Should be $true
112+
(Get-Date -Format F).Contains(",") -and (Get-Date -Format F).Contains(":") | Should be $true
113+
(Get-Date -Format g).Contains("/") -and (Get-Date -Format g).Contains(":") | Should be $true
114+
(Get-Date -Format G).Contains("/") -and (Get-Date -Format G).Contains(":") | Should be $true
115+
(Get-Date -Format m).Contains(",") -or `
116+
(Get-Date -Format m).Contains(":") -or `
117+
(Get-Date -Format m).Contains("/") | Should be $false
102118
}
103119

104120
It "Should check that Get-Date can return the correct datetime from the system time" {
105-
$timeDifference = $(Get-Date).Subtract([System.DateTime]::Now)
121+
$timeDifference = $(Get-Date).Subtract([System.DateTime]::Now)
106122

107-
$timeDifference.Days | Should Be 0
108-
$timeDifference.Hours | Should Be 0
109-
$timeDifference.Minutes | Should Be 0
110-
$timeDifference.Milliseconds | Should BeLessThan 1
111-
$timeDifference.Ticks | Should BeLessThan 10000
123+
$timeDifference.Days | Should Be 0
124+
$timeDifference.Hours | Should Be 0
125+
$timeDifference.Minutes | Should Be 0
126+
$timeDifference.Milliseconds | Should BeLessThan 1
127+
$timeDifference.Ticks | Should BeLessThan 10000
112128
}
113-
114129
}

0 commit comments

Comments
 (0)