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
  • 13,637
  • 40
  • 117
  • 214
  • 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
  • 41,623
  • 16
  • 83
  • 111
  • 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'
)

# -> @('Startup', 'Shutdown', ...)
($MyInvocation.MyCommand.Parameters['Type'].Attributes |
  Where-Object { $_ -is [ValidateSet] }).ValidValues

A simpler, but potentially 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:

      • You can explicitly run Set-StrictMode -Off or Set-StrictMode -Version 1 at the start of your script / function, though you may want to restore the desired value afterwards. Whatever mode you set will affect descendant scopes too. Note that that there is no way to query the strict mode currently in effect.

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

  • that running into this bug (still present as of PowerShell 7.3.1) when repeatedly dot-sourcing a script is not a concern.

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

# Assumes that at most Set-StrictMode -Version 1 is in effect.
# You could explicitly run Set-StrictMode -Off or Set-StrictVersion -Version 1 
# in here first.
(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))

mklement0
  • 382,024
  • 64
  • 607
  • 775
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
  • 219
  • 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
  • @mklement0, what "unrelated benefits" does this approach provide that are not done by `ValidateSet()`? – lit Jan 05 '23 at 17:26
  • @lit, I was referring to the additional validation that a script-block-based approach can provide that Eric mentioned. – mklement0 Jan 05 '23 at 17:34