6

I was wondering if there was a way to retrieve the values used in the clause Param() for ValidateSet. Something like this would be great:

Function Foo {
    Param (
        [ValidateSet('Startup', 'Shutdown', 'LogOn', 'LogOff')]
        [String]$Type = 'Startup'
    )

    $Type.ValidateSet
}

But of course there is no such property on the Type object. Is it possible to retrieve the values set in ValidateSet?

DarkLite1
  • 12,007
  • 34
  • 105
  • 185
  • What about using `Get-Command Foo | Select -Expand Definition` to extract the validate set from the resulting string? – Manuel Batsching Mar 09 '17 at 13:58
  • The thing is I use the `Param()` clause at the top of my script. so that would be difficult. I need to be able to find it in the script itself if it is at all possible. But thanks for the tip. – DarkLite1 Mar 09 '17 at 14:01

3 Answers3

7
function Foo {
    param (
        [ValidateSet('Startup', 'Shutdown', 'LogOn', 'LogOff')]
        [String]$Type = 'Startup'
    )

    $ParameterList = (Get-Command -Name $MyInvocation.MyCommand).Parameters
    $ParameterList["Type"].Attributes.ValidValues
}

After your comment:

param (
        [ValidateSet('Startup', 'Shutdown', 'LogOn', 'LogOff')]
        [String]$Type = 'Startup'
)


(Get-Variable "Type").Attributes.ValidValues

The Get-Variable call also works in a function.

David Brabant
  • 39,073
  • 16
  • 82
  • 103
  • Works perfectly! Except when I do a `Set-Location` to a network folder before I run the code. Really weird... – DarkLite1 Mar 09 '17 at 14:44
  • @DarkLite1: works for me ... (PowerShell 5 on Windows 10) – David Brabant Mar 09 '17 at 14:51
  • Correct, the first time I run the code it works fine. When I run it a second time in the ISE it's not generating any output. Really strange... Win 7 PS 4.0. – DarkLite1 Mar 09 '17 at 15:02
  • Nicely done, but the `Get-Command -Name` part is both unnecessary and brittle. Omitting it makes the first snippet work in both functions and scripts, except if `Set-StrictMode` is set to `-version 2` or higher. It's implied by your last sentence, but just to state it explicitly: The simpler `Get-Variable` approach works in both scripts and functions as well (but the `Set-StrictMode` caveat applies). – mklement0 Mar 09 '17 at 18:05
  • Thx for the great feedback! Indeed, opened a new [question](http://stackoverflow.com/questions/42712595/why-does-get-variable-only-works-the-first-time) for this. – DarkLite1 Mar 10 '17 at 07:25
6

All solutions below work in both functions and scripts.

Most robust solution that should work in any invocation scenario, PSv2+:

param (
    [ValidateSet('Startup', 'Shutdown', 'LogOn', 'LogOff')]
    [String]$Type = 'Startup'
)

($MyInvocation.MyCommand.Parameters['Type'].Attributes |
  Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues

A simpler, but fragile PSv3+ solution, which assumes:

  • that Set-StrictMode is either set to -version 1 or not set.

    • Set-StrictMode may have been set outside of your control, so if you don't fully control the execution environment, it is safer to use the more verbose, PSv2-compatible command above.
      (The Set-StrictMode setting behaves like a variable: it is inherited by descendent scopes, but setting it in a descendent scope sets it locally (only affects that scope and its descendants).)

    • However, if you define a function as part of a module, the outside world's Set-StrictMode setting does not apply.

  • that, up to at least Windows PowerShell v5.1 / PowerShell Core v6.0-alpha16, running into this bug when repeatedly dot-sourcing a script is not a concern.

param (
    [ValidateSet('Startup', 'Shutdown', 'LogOn', 'LogOff')]
    [String]$Type = 'Startup'
)

(Get-Variable Type).Attributes.ValidValues

Optional background information

The PSv3+ shorthand syntax (Get-Variable Type).Attributes.ValidValues is essentially the equivalent of:

(Get-Variable Type).Attributes | ForEach-Object { $_.ValidValues }

That is, PowerShell automatically enumerates the collection .Attributes and collects the values of each element's .ValidValues property.

In the case at hand, only one attribute in the .Attributes collection - the one of subtype [System.Management.Automation.ValidateSetAttribute] - has a .ValidValues property, so that single value is returned.

Given that the other attributes have no such property, setting Set-StrictMode to -version 2 or higher causes the attempt to access a nonexistent property to raise an error, and the command fails.

((Get-Variable Type).Attributes |
  Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues

bypasses this problem by explicitly targeting the one attribute of interest (using the -is operator to identify it by type) that is known to have a .ValidValues property.

The more verbose alternative to accessing the attributes of parameter [variable] $Type with (Get-Variable Type).Attributes is to use $MyInvocation.MyCommand.Parameters['Type'].Attributes.

Use of the $MyInvocation.MyCommand.Parameters collection enables enumerating and inspecting all parameters without needing to know their names in advance.


David Brabant's answer is helpful, but (as of this writing):

  • It may create the mistaken impression that separate approaches are needed for scripts and functions.

  • The Get-Command -Name $MyInvocation.MyCommand part is:

    • unnecessary, because $MyInvocation.MyCommand itself provides the information of interest:
      $MyInvocation.MyCommand is an instance of type [System.Management.Automation.ExternalScriptInfo] in scripts, and type [System.Management.Automation.FunctionInfo] in functions, both of which derive from type [System.Management.Automation.CommandInfo], which is the type that Get-Commmand returns - so not only do they provide the same information, they also unambiguously refer to the enclosing script/function.

    • brittle:

      • $MyInvocation.MyCommand is converted to a string due to being passed to the -Name parameter, which in a script results in the script's mere filename (e.g., script.ps1), and in a function in the function's name (e.g., Foo).

      • In a script, this will typically cause Get-Command not to find the script at all - unless that script happens to be in the PATH (one of the directories listed in $env:PATH). But that also means that a different script that happens to have the same filename and that happens to be / come first in the PATH may be matched, yielding incorrect results.
        In short: Get-Command -Name $MyInvocation.MyCommand in scripts will often break, and when it does return a result, it may be for the wrong script.

      • In a function, it can identify the wrong command too, although that is much less likely:
        Due to PowerShell's command precedence, a given name is first interpreted as an alias, and then as a function, so, in theory, with a Foo alias defined, Get-Command -Name $MyInvocation.MyCommand inside function Foo would mistakenly return information about the alias.
        (It's nontrivial to invoke function Foo while alias Foo is defined, but it can be done; e.g.: & (Get-Item Function:Foo))

Community
  • 1
  • 1
mklement0
  • 312,089
  • 56
  • 508
  • 622
0

validateScript, can provide a more flexible solution and would work well if you needed additional parameter validation. This also allows you to get a list of the valid parameters outside of the foo function, with the creation of the get-validTypes function.

Function Foo {
    Param (
        [validateScript({test-validTypes $_})]
        [String]$Type = 'Startup'
    )

    get-validTypes
}

function get-validTypes {

    $powerOptions = @('Startup', 'Shutdown', 'LogOn', 'LogOff')
    Write-Output $powerOptions

}

function test-validTypes {
[cmdletbinding()]
param ($typeInput)

    $validTypes = get-validTypes
    if ($validTypes.contains($typeInput)){
        return $true
    } else {
        Write-Error "Invalid Type Paramater, Must be on of the following: $powerOptions"
    }

}
Eric
  • 209
  • 1
  • 6
  • I didn't down-vote, but this does seem like an unnecessarily cumbersome workaround (where none is needed) that makes the code less readable. – mklement0 Mar 09 '17 at 15:31
  • 1
    @mklement0 I agree, but don't plan on removing the answer because it is a valid approach if additional validation is needed. – Eric Mar 09 '17 at 18:54
  • Understood. It might help if you made it clearer that you're not directly answering the question, but suggesting an alternative because it has unrelated benefits. – mklement0 Mar 10 '17 at 14:07