Powershell: Improve class constructor for IntelliSense support - powershell

I worte this class with two custom constructors:
class ExtensionApp {
[String] $name
[String] $version
[String] $path
[switch] $isContainerPath
[switch] $useNugetDownloader
[switch] $force
[switch] $skipVerification
ExtensionApp() {}
ExtensionApp(
[String] $name,
[String] $version,
[switch] $useNugetDownloader,
[switch] $force,
[switch] $skipVerification
) {
$this.name = $name
$this.version = $version
$this.useNugetDownloader = $useNugetDownloader
$this.force = $force
$this.skipVerification = $skipVerification
if (($this.version -eq '') -or ($null -eq $this.version)) {
#do something
}
}
ExtensionApp(
[String] $name,
[switch] $isContainerPath,
[String] $path,
[switch] $force,
[switch] $skipVerification
) {
$this.name = $name
$this.path = $path
$this.isContainerPath = $isContainerPath
$this.force = $force
$this.skipVerification = $skipVerification
}
}
I'm using those Objects to fill a list i want to process later which looks something like this atm:
$CustApps = New-Object Collections.Generic.List[ExtensionApp]
$CustApps.Add([ExtensionApp]::new('Extension A', '10.0.2554.0', $true , $false, $false)) #first constructor
$CustApps.Add([ExtensionApp]::new('Extension b', $false, '\\server\folder\file.app' , $false, $false)) #second constructor
Thing is: I don't like the way i have to use the constuctors because you dont' get any Intellisense support, and if you have to mix up the constructors while filling the same List, it gets messy.
I could just define a function like this:
function CreateExtensionApp {
param(
[String] $name,
[String] $version,
[String] $path,
[switch] $isContainerPath,
[switch] $useNugetDownloader,
[switch] $force,
[switch] $skipVerification
)
$new = [ExtensionApp]::new()
$new.name = $name
$new.version = $version
$new.path = $path
$new.isContainerPath = $isContainerPath
$new.useNugetDownloader = $useNugetDownloader
$new.force = $force
$new.skipVerification = $skipVerification
if (($true -eq $new.useNugetDownloader) -and (($new.version -eq '') -or ($null -eq $new.version))) {
Get-LogiVersion -PackageId $this.name
}
return $new
}
and use that one like this:
$CustApps = New-Object Collections.Generic.List[ExtensionApp]
$CustApps.Add((New-ExtensionAppObj -name 'Extension A' -version '10.0.2554.0' -useNugetDownloader))
$CustApps.Add((New-ExtensionAppObj -name 'Extension B' -path '\\server\folder\file.app' -force -skipVerification))
but i'm kind of a perfectionist and in my opinion, this function should be a static method belonging to the class itself. - I also found a way to do that but it would deprive me of the parameter-names and intellisense support again.
is there a solution that would allow me to use the parameter-names AND have all the code in the class definition?

I can't offer a solution, but I'll try to shed some light on the challenges involved:
IntelliSense:
As of v2022.6.3 of the PowerShell extension for Visual Studio Code, IntelliSense support for calling methods is limited to read-only display of all overloads, and is only shown when the method name has been typed, before typing (
In other words, there is currently no support for:
(a) showing an overload signature (parameter names) as arguments are being typed.
(b) allowing selecting one of the overloads so as to auto-complete a call to that overload with argument placeholders named for the parameters.
GitHub issue #1356 discusses potentially improving method-call IntelliSense in the future:
(a) is problematic, given PowerShell's lack of static typing, though it's conceivable to let the user manually cycle through available overloads in the same way that the C# extension does, albeit without type awareness.
(b) may be feasible, if the IntelliSense feature allows calling a Visual Studio Code snippet in response to a user selection.
Self-documenting code:
Since all (declared) parameters in PowerShell cmdlets / functions / scripts have names, you always have the option to spell out the target parameter for each argument (though there's often also support for positional arguments for a subset of parameters).
Unlike C#, PowerShell, as of PowerShell 7.2.x, does not support named arguments in method calls:
That is, the following C# sample call currently has no PowerShell equivalent:
C#: string.Compare(strA: "foo", strB: "FOO", ignoreCase: true)
PowerShell: [string]::Compare(strA: "foo", strB: "FOO", ignoreCase: $true)
GitHub issue #13307 proposes adding support for named arguments.

Related

Using ValidateSet with functions or List

I'm trying to do something like this.
The Get-SomeOtherParameter returns a system.Array type list from a database.
I don't want to hardcode my ValidateSet in case the list changes overTime in the database
function Get-SomeItems {
param (
[Parameter(Mandatory = $true)]
[ValidateSet(Get-SomeOtherParameter)]
[string]$filter,
[Parameter(Mandatory = $true)]
[ValidateSet('abc', 'def', 'ghi')]
[String]$filter2
)
}
To complement Start-Automating's helpful answer by spelling out the [ValidateScript({ ... }] and [ArgumentCompleter({ ... }) approaches:
# Function that returns the valid values for the -filter parameter below.
function Get-ValidFilterValues {
# Sample, hard-coded values. This is where your database lookup would happen.
'foo', 'bar'
}
function Get-SomeItems {
param (
[Parameter(Mandatory)]
[ValidateScript({
$validValues = Get-ValidFilterValues
if ($_ -in $validValues) { return $true } # OK
throw "'$_' is not a valid value. Use one of the following: '$($validValues -join ', ')'"
})]
[ArgumentCompleter({
param($cmd, $param, $wordToComplete)
(Get-ValidFilterValues) -like "$wordToComplete*"
})]
[string]$filter,
[Parameter(Mandatory)]
[ValidateSet('abc', 'def', 'ghi')]
[String]$filter2
)
$filter, $filter2 # sample output.
}
A simpler PowerShell (Core) 7+ alternative is to implement validation via a custom class that implements the System.Management.Automation.IValidateSetValuesGenerator interface, which automatically also provides tab-completion:
# Custom class that implements the IValidateSetValuesGenerator interface
# in order to return the valid values for the -filter parameter below.
class ValidFilterValues : System.Management.Automation.IValidateSetValuesGenerator {
[string[]] GetValidValues() {
# Sample, hard-coded values. This is where your database lookup would happen.
return 'foo', 'bar'
}
}
function Get-SomeItems {
param (
[Parameter(Mandatory)]
[ValidateSet([ValidFilterValues])] # Pass the custom class defined above.
[string]$filter,
[Parameter(Mandatory)]
[ValidateSet('abc', 'def', 'ghi')]
[String]$filter2
)
$filter, $filter2 # sample output.
}
There's two aspects to what you're trying to do:
Making sure the parameter validation is correct
Making the PowerShell experience around it "good" (aka supporting tab completion).
Parameter Validation :
As you might have already noticed [ValidateSet] is a hard-coded list. It's not really possible to soft code this (it is possible to dynamically build your script every time using some other modules, lemme know if you want more of an explainer for this).
To make the Validation work without [ValidateSet], I'd suggest [ValidateScript({})]. [ValidateScript] will run whatever script is in ValidateScript to ensure the script is valid. If the [ValidateScript()] throws, the user will see that message when they pass an invalid value in.
Tab-Completion :
To make it feel easy, you'll also want to add support for tab completion.
This is fairly straightforward using the [ArgumentCompleter] attribute.
Here's an example copied / pasted from a module called LightScript
[ArgumentCompleter({
param ( $commandName,
$parameterName,
$wordToComplete,
$commandAst,
$fakeBoundParameters )
$effectNames = #(Get-NanoLeaf -ListEffectName |
Select-Object -Unique)
if ($wordToComplete) {
$toComplete = $wordToComplete -replace "^'" -replace "'$"
return #($effectNames -like "$toComplete*" -replace '^', "'" -replace '$',"'")
} else {
return #($effectNames -replace '^', "'" -replace '$',"'")
}
})]
This ArgumentCompleter does a few things:
Calls some other command to get a list of effects
If $wordToComplete was passed, finds all potential completions (while stripping off whitespace and enclosing in quotes)
If $WordToComplete was not passed, puts each potential completion in quotes
Basically, all you should need to change are the command names / variables to make this work.
Hope this Helps

String parameter is not resolved when parameterizing Mock ParameterFilter in Pester

I am using Pester testing library (with version 5.0.2) to test my PowerShell scripts (with version 5.1) and mock its dependencies.
Pester has a Mock method which can be used to mock dependencies. More info here.
I am trying to create a helper method wrapping this Mock method, to make my code more readable:
Function MockVstsInput {
Param(
[Parameter(Mandatory=$true, Position=1)]
[string]$inputName,
[Parameter(Mandatory=$true, Position=2)]
[string]$returnValue
)
Mock Get-VstsInput {return $returnValue} -ParameterFilter { $Name -eq $inputName}
}
In this helper method I am mocking the dependency Get-VstsInput which has a parameter $Name
Then I use this code in my test script:
MockVstsInput "targetapi" "api-name"
Meaning that if the Get-VstsInput is called with $Name param "targetapi" then it should return "api-name". Usually such parameterizing works in other languages (f.e.: C# or Java), but here the $inputName string is not resolved in the MockVstsInput method.
When I call the Get-VstsInput method in my production code:
$newapi=Get-VstsInput -Name targetapi
Then in the log I have the following Mock information:
Mock: Running mock filter { $Name -eq $inputName } with context: Name = targetapi.
Mock: Mock filter did not pass.
Where we can see that the $inputName string is not resolved in my scriptblock, so the mocking does not happen.
What I have tried so far, with no success:
swapping the members of the equal comparison in the predicate scriptblock { $inputName -eq $Name}
using $ExecutionContext.InvokeCommand.ExpandString($inputName) in the script block to resolve $inputName string
creating a script block with [ScriptBlock]::create($Name -eq $inputName) then using this in the -ParameterFilter
last but not least I tried to call GetNewClosure of my script block, but it did not help either: { $Name -eq $inputName}.GetNewClosure()
What do you think what the root cause of my problem is? Thanks in advance for all the help!
You are close. It's all in how you build the needed scriptblocks.
For the ParameterFilter scriptblock you need to escape $Name with a backtick so that it gets created as a variable. $inputName will be replaced with our variable value so you need to surround in quotes.
# $inputName = 'Daniel'
$sb_param = [scriptblock]::Create("`$Name -eq '$inputName'")
This way the final statement inside the scriptblock looks like
{ $Name -eq 'Daniel' }
Similarly, in our MockWith block, we need to also surround with quotes or the scriptblock will not include a string, but an invalid command
# $returnValue = 'Hi Daniel'
$sb_return = [scriptblock]::Create("'$returnValue'")
becomes
{ 'Hi Daniel' }
if we don't include the quotes when we create the scriptblock, the scriptblock will contain the statement Hi Daniel which will fail as command not found
Here is everything in a working example
Describe 'Setting up a Mock from a Function' {
BeforeAll {
Function Get-VstsInput {
[CmdletBinding()]
Param(
$Name
)
'Not Mocked'
}
Function MockVstsInput {
Param(
[Parameter(Mandatory = $true, Position = 1)]
[string]$inputName,
[Parameter(Mandatory = $true, Position = 2)]
[string]$returnValue
)
$sb_param = [scriptblock]::Create("`$Name -eq '$inputName'")
$sb_return = [scriptblock]::Create("'$returnValue'")
Mock Get-VstsInput -MockWith $sb_return -ParameterFilter $sb_param
}
MockVstsInput -inputname 'Daniel' -returnValue 'Hi Daniel'
}
It 'Should mock' {
$test = Get-VstsInput -Name 'Daniel'
$test | Should -Be 'Hi Daniel'
}
}
To clarify, this is a scoping issue. What is happening is that you are providing these scriptblocks to the mock command to run at a later time in a different scope. When you run the mocked command the variables $inputName and $returnValue are not defined in those scriptblocks when they are invoked. These variables were only available in the MockVstsInput function and were cleaned up once the function completed.
To illustrate this, the following code will work, but I do not recommend doing this way because if you run the function more than once to define different Mocks you will be overriding the global variables each time affecting any previously defined Mocks
Function MockVstsInput {
Param(
[Parameter(Mandatory = $true, Position = 1)]
[string]$inputName,
[Parameter(Mandatory = $true, Position = 2)]
[string]$returnValue
)
$global:mockInputName = $inputName
$global:mockReturnValue = $returnValue
Mock Get-VstsInput -ParameterFilter {$Name -eq $global:mockInputName} -MockWith {$global:mockReturnValue}
}
So instead of giving the 2 parameters scriptblocks with variables to later be resolved which will no longer be in scope the solution is to create the scriptblocks with the values of our variables hardcoded.
Also, see this answer for a way to do it using BeforeEach { }

Couldn't use predefined array inside Validateset - Powershell

I'm looking for a way to make a cmdlet which receives parameter and while typing, it prompts suggestions for completion from a predefined array of options.
I was trying something like this:
$vf = #('Veg', 'Fruit')
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateSet($vf)]
$Arg
)
}
The expected result should be:
When writing 'Test-ArgumentCompleter F', after clicking the tub button, the F autocompleted to Fruit.
To complement the answers from #mklement0 and #Mathias, using dynamic parameters:
$vf = 'Veg', 'Fruit'
function Test-ArgumentCompleter {
[CmdletBinding()]
param ()
DynamicParam {
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$AttributeCollection.Add($ParameterAttribute)
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($vf)
$AttributeCollection.Add($ValidateSetAttribute)
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter('Arg', [string], $AttributeCollection)
$RuntimeParameterDictionary.Add('Arg', $RuntimeParameter)
return $RuntimeParameterDictionary
}
}
Depending on how you want to predefine you argument values, you might also use dynamic validateSet values:
Class vfValues : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() { return 'Veg', 'Fruit' }
}
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateSet([vfValues])]$Arg
)
}
note: The IValidateSetValuesGenerator class [read: interface] was introduced in PowerShell 6.0
In addition to mklement0's excellent answer, I feel obligated to point out that in version 5 and up you have a slightly simpler alternative available: enum's
An enum, or an "enumeration type", is a static list of labels (strings) associated with an underlying integral value (a number) - and by constraining a parameter to an enum type, PowerShell will automatically validate the input value against it AND provide argument completion:
enum MyParameterType
{
Veg
Fruit
}
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[MyParameterType]$Arg
)
}
Trying to tab complete the argument for -Arg will now cycle throw matching valid enum labels of MyParameterType:
PS ~> Test-ArgumentCompleter -Arg v[<TAB>]
# gives you
PS ~> Test-ArgumentCompleter -Arg Veg
PowerShell generally requires that attribute properties be literals (e.g., 'Veg') or constants (e.g., $true).
Dynamic functionality requires use of a script block (itself specified as a literal, { ... }) or, in specific cases, a type literal.
However, the [ValidateSet()] attribute only accepts an array of string(ified-on-demand) literals or - in PowerShell (Core) v6 and above - a type literal (see below).
Update:
If you're using PowerShell (Core) v6+, there's a simpler solution based on defining a custom class that implements the System.Management.Automation.IValidateSetValuesGenerator interface - see the 2nd solution in iRon's helpful answer.
Even in Windows PowerShell a simpler solution is possible if your validation values can be defined as an enum type - see Mathias R. Jessen's helpful answer.
To get the desired functionality based on a non-literal array of values, you need to combine two other attributes:
[ArgumentCompleter()] for dynamic tab-completion.
[ValidateScript()] for ensuring on command submission that the argument is indeed a value from the array, using a script block.
# The array to use for tab-completion and validation.
[string[]] $vf = 'Veg', 'Fruit'
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
# Tab-complete based on array $vf
[ArgumentCompleter({
param($cmd, $param, $wordToComplete) $vf -like "$wordToComplete*"
})]
# Validate based on array $vf.
# NOTE: If validation fails, the (default) error message is unhelpful.
# You can work around that in *Windows PowerShell* with `throw`, and in
# PowerShell (Core) 7+, you can add an `ErrorMessage` property:
# [ValidateScript({ $_ -in $vf }, ErrorMessage = 'Unknown value: {0}')]
[ValidateScript({
if ($_ -in $vf) { return $true }
throw "'$_' is not in the set of the supported values: $($vf -join ', ')"
})]
$Arg
)
"Arg passed: $Arg"
}
To add to the other helpful answers, I use something similiar for a script I made for work:
$vf = #('Veg', 'Fruit','Apple','orange')
$ScriptBlock = {
Foreach($v in $vf){
New-Object -Type System.Management.Automation.CompletionResult -ArgumentList $v,
$v,
"ParameterValue",
"This is the description for $v"
}
}
Register-ArgumentCompleter -CommandName Test-ArgumentCompleter -ParameterName Arg -ScriptBlock $ScriptBlock
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[String]$Arg )
}
Documentation for Register-ArgumentCompleter is well explained on Microsoft Docs. I personally don't like to use the enum statement as it didnt allow me to uses spaces in my Intellisense; same for the Validate parameter along with nice features to add a description.
Output:
EDIT:
#Mklement made a good point in validating the argument supplied to the parameter. This alone doesnt allow you to do so without using a little more powershell logic to do the validating for you (unfortunately, it would be done in the body of the function).
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
$Arg )
if($PSBoundParameters.ContainsKey('Arg')){
if($VF -contains $PSBoundParameters.Values){ "It work:)" }
else { "It no work:("}
}
}
I think it's worth sharing another alternative that complements the helpful answers from mklement0, Mathias, iRon and Abraham. This answer attempts to show the possibilities that PowerShell can offer when it comes to customization of a Class.
The Class used for this example offers:
Compatibility with Windows PowerShell 5.1 and PowerShell Core.
Validation, Completion and custom Error Message.
For the example below I'll be using completion and validation on values from a current directory, the values are fed dynamically at runtime with Get-ChildItem -Name.
Class
When referring to the custom validation set I've decided to use the variable $this, however that can be easily change for a variable name of one's choice:
[psvariable]::new('this', (& $this.CompletionSet))
The completion set could be also a hardcoded set, i.e.:
[string[]] $CompletionSet = 'foo', 'bar', 'baz'
However that would also require some modifications in the class logic itself.
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Collections
using namespace System.Collections.Generic
class CustomValidationCompletion : ValidateEnumeratedArgumentsAttribute, IArgumentCompleter {
[scriptblock] $CompletionSet = { Get-ChildItem -Name }
[scriptblock] $Validation
[scriptblock] $ErrorMessage
CustomValidationCompletion() { }
CustomValidationCompletion([scriptblock] $Validation, [scriptblock] $ErrorMessage) {
$this.Validation = $Validation
$this.ErrorMessage = $ErrorMessage
}
[void] ValidateElement([object] $Element) {
$context = #(
[psvariable]::new('_', $Element)
[psvariable]::new('this', (& $this.CompletionSet))
)
if(-not $this.Validation.InvokeWithContext($null, $context)) {
throw [MetadataException]::new(
[string] $this.ErrorMessage.InvokeWithContext($null, $context)
)
}
}
[IEnumerable[CompletionResult]] CompleteArgument(
[string] $CommandName,
[string] $ParameterName,
[string] $WordToComplete,
[CommandAst] $CommandAst,
[IDictionary] $FakeBoundParameters
) {
[List[CompletionResult]] $result = foreach($item in & $this.CompletionSet) {
if(-not $item.StartsWith($wordToComplete)) {
continue
}
[CompletionResult]::new("'$item'", $item, [CompletionResultType]::ParameterValue, $item)
}
return $result
}
}
Implementation
function Test-CompletionValidation {
[alias('tcv')]
[CmdletBinding()]
param(
[CustomValidationCompletion(
Validation = { $_ -in $this },
ErrorMessage = { "Not in set! Must be one of these: $($this -join ', ')" }
)]
[ArgumentCompleter([CustomValidationCompletion])]
[string] $Argument
)
$Argument
}
Demo

Powershell splatting operator only for accepted parameters?

Is it possible, using PowerShell, to use splatting from hashtable when the hashtable contains more entries that the function accepts ?
My use case is to have config objects I pass from one function to another. However, all functions does not require same parameters.
Ex:
function Process-Something{
param(
[Parameter()]
[string]$Owner
)
}
function Process-SomethingElse{
param(
[Parameter()]
[string]$Owner,
[Parameter()]
[int]$x,
[Parameter()]
[int]$y
)
}
$config = #{
"Owner" = "Bart Simpson"
"X" = 10
"Y" = 20
}
Process-Something #config
Process-SomethingElse #config
It fails with these error:
Process-Something : Cannot find a matching parameter « Y ».
The idea is to avoid specifying individual properties for each functions.
As #Ansgar is stating in the comments, the whole idea of having defined your parameters, is to get validation. When you are splatting parameters to your function, you are forcing them to the function. So if a given property of your hashtable doesn't exist as a parameter, you will get an error - just like it is intended.
What you can do, is going into a PSCustomObject and utilize the pipe. If you set all you parameters to accept value from the pipeline, using property name (ValueFromPipelineByPropertyName = $true), then you can actually get the desired behavior.
First I'm redefining your different functions, to have the ValueFromPipelineByPropertyName = $true parameter attribute configured.
function Process-Something{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Owner
)
$PSBoundParameters
}
function Process-SomethingElse{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Owner,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int]$x,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int]$y
)
$PSBoundParameters
}
With that in place, I'm able to create a hashtable like your example, convert it to a PSCustomObject, and now I can pipe that new object to the different methods and have them pick up only the properties that they need.
I included the PSBoundParameters to showcase that they get what they expect.
Testing is done like this:
$config = #{
"Owner" = "Bart Simpson"
"X" = 10
"Y" = 20
}
$psConfig = [PSCustomObject]$config
$psConfig | Process-Something
$psConfig | Process-SomethingElse

PowerShell Param ValidateSet values with Spaces and Tab completion

First, I do apologize for posting another question concerning PowerShell and tab completion. The StackOverflow system identified several excellent questions with answers concerning this very topic, but they all seemed too cumbersome to implement into this simple New-ADComputer script.
The params are going into a Splat to keep the script readable. The following code correctly tab completes in the ISE, but must be wrapped in double quotes.
Is there any native method in PowerShell to allow for tab completion of Parameter Sets that include spaces?
Param(
[Parameter(Mandatory=$true)]
[string]$Server,
[Parameter(Mandatory=$true)]
[ValidateSet('Env1','Env 2','Env 3')]
[string]$Environment,
[Parameter(Mandatory=$true)]
[ValidateSet('Application','Database','File and Print','Web Server')]
[string]$Type
)
$NewADitems = #{
Name = $server
Path = "OU=$Type,OU=$Environment,OU=Smaller DN string"
Location ='MySite'
Description = "Test Description"
ManagedBy = "Huge Distingushed Name string"
WhatIf = $true
}
Write-Host #NewADitems
Command used and error received:
PS C:\Scripts> .\ADComputer-ParamTest.ps1 -Server ThisTest -Environment Env 3 -Type File and Print
C:\Scripts\ADComputer-ParamTest.ps1 : Cannot validate argument on parameter
'Environment'. The argument "Env" does not belong to the set "Env1,Env 2,Env3"
specified by the ValidateSet attribute. Supply an argument that is in the
set and then try the command again.At line:1 char:58
+ .\ADComputer-ParamTest.ps1 -Server ThisTest -Environment Env 3 -Type File and Pr ...
+ ~~~
Edit: More information. If you leave off the single/double quotes in my example script for the parameter Environment, tab completion will not work for the final parameter Type. Enclosing the 2nd set in quotes will correct this but it's a way to keep watch for this behavior.
No, at least up to Powershell 5.0 April 2015 preview. Tab completion works as you describe. It will still need the quotes around the set to actually work without throwing the error. For what it's worth, it does add the closing quote of the matching type when you start the tab completion with a quote. For example, pressing "f then Tab will complete to "File and Print"(not sure when that was added as a feature).
I tried finding ways to auto-include the quotes as part of the ValidateSet including additional double quotes around the parameter sets and other attempts at escaping quotes. All attempts resulted in tab completion not working in various ways.
Some of the attempts, in case anyone might try that avenue:
[ValidateSet('Env1','"Env 2"','"Env 3"')]
[ValidateSet('Env1',"'Env 2'","'Env 3'")]
[ValidateSet('Env1','`"Env 2`"',"`'Env 3`'")]
[ValidateSet('Env1','\"Env 2\"',"\'Env 3\'")]
This has been entered as a bug since 2013. According to the workarounds listed in Auto-completed parameter values, with spaces, do not have quotes around them, you can update the TabExpansion2 function that Powershell uses for autocompletion. To do so, just run the following code:
function TabExpansion2
{
[CmdletBinding(DefaultParameterSetName = 'ScriptInputSet')]
Param(
[Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 0)]
[string] $inputScript,
[Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 1)]
[int] $cursorColumn,
[Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 0)]
[System.Management.Automation.Language.Ast] $ast,
[Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 1)]
[System.Management.Automation.Language.Token[]] $tokens,
[Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 2)]
[System.Management.Automation.Language.IScriptPosition] $positionOfCursor,
[Parameter(ParameterSetName = 'ScriptInputSet', Position = 2)]
[Parameter(ParameterSetName = 'AstInputSet', Position = 3)]
[Hashtable] $options = $null
)
End
{
if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet')
{
$completion = [System.Management.Automation.CommandCompletion]::CompleteInput(
$inputScript,
$cursorColumn,
$options)
}
else
{
$completion = [System.Management.Automation.CommandCompletion]::CompleteInput(
$ast,
$tokens,
$positionOfCursor,
$options)
}
$count = $completion.CompletionMatches.Count
for ($i = 0; $i -lt $count; $i++)
{
$result = $completion.CompletionMatches[$i]
if ($result.CompletionText -match '\s')
{
$completion.CompletionMatches[$i] = New-Object System.Management.Automation.CompletionResult(
"'$($result.CompletionText)'",
$result.ListItemText,
$result.ResultType,
$result.ToolTip
)
}
}
return $completion
}
}
It's worth noting that string insertion works properly for native cmdlets like Get-EventLog -LogName which will properly encase 'Internet Explorer'. Although if you look at the source for Get-EventLog, you'll see that $LogName doesn't actually use ValidateSet so it's intellisense must be provided through another mechanism.
Other Instances:
ValidateSet and tab completion does not work on strings with spaces