<img height="1" width="1" src="https://www.facebook.com/tr?id=1529264867168163&amp;ev=PageView &amp;noscript=1">
blog_listing_hero_img.jpg

Powershell: Upping your Parameter Validation Game with Dynamic Parameters Part II

Now that you've got an understanding of Powershell's advanced functions and the ValidateSet() parameter validation method in the first part of my blog, “Validating Powershell Advanced Function Parameters” you can begin Part #2 of my small post series. Part 2 of this series goes deeper by demonstrating how to dynamically create your sets for ValidateSet() so they aren't hardcoded in. This is essential when dealing with values that may constantly change or even if you just want to practice writing good scripting and have no static references.

So now that I've dazzled you with the magic tab-completion of parameter attributes with ValidateSet() in my last post let's take it one step farther. In that simple instance, I only had 2 values to filter on; True and False. Simply typing them out is easy enough but what if the values you'd like to use aren't so cut and dry? Let me give you another real-world example I just finished today.

I had a need to create a function around the Set-Acl cmdlet. I needed the ability to easily change permissions on files and folders. I found a great example but I needed to set permissions on a ton of files/folders. Also, I really didn't want to have to remember the entire the 4 lines it took to get this done so I decided to create an advanced function to help me out. In my new function I just wanted to type Set-MyFileSystemAcl and add a few parameters like the username and what kind of access I'd like that username to have.

When I finished with my function it looked something like this:

[powershell]
function Set-MyFileSystemAcl {
[CmdletBinding()]
[OutputType()]
param (
[Parameter(Mandatory)]
[ValidateScript({ Test-Path -Path $_ })]
[string]$Path, [Parameter(Mandatory)]
[string]$Identity, [Parameter(Mandatory)]
[string]$Right, [Parameter(Mandatory)]
[string]$InheritanceFlags,
[Parameter(Mandatory)]
[string]$PropagationFlags,
[Parameter(Mandatory)]
[string]$Type
)
process {
try {
$Acl = Get-Acl $Path
#$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule('Everyone', 'FullControl', 'ContainerInherit,ObjectInherit', 'NoPropagateInherit', 'Allow')
$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule($Identity, $Right, $InheritanceFlags, $PropagationFlags, $Type)
$Acl.SetAccessRule($Ar)
Set-Acl $Path $Acl
} catch {
Write-Error $_.Exception.Message
}
}
}
[/powershell]

Looks simple enough, right? Just your standard, run-of-the-mill Powershell advanced function with a few parameters. Take a closer look at the parameters. Do you know what you'd specify for the parameter values for the $Right, $InheritanceFlags, $PropagationFlags or $Type? I had no idea what I could use there. Sure, I could have created some monster comment block at the top explaining all the appropriate values that each parameter could have or just link to the MSDN page covering the FileSystemAccessRule constructor but that wouldn't be too kind to the next person to use this function. I'd rather have my nice, pretty tab-completion of each parameter value so that I know exactly what I could use. There's one problem though. I'm not about to copy/paste all the values from each of the .NET classes that each parameter represents. Even if I did, what if those values change sometime? My function would then be broken. I need to figure out a way to dynamically create my ValidateSet() set rather than statically assigning a specific set. ...introducing dynamic parameters!

Dynamic parameters give you a way to dynamically create the set of values ValidateSet() uses but they are a major pain in the ass to get your head around. Take a look at this code:

[powershell]
$AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
$ParamAttrib.Mandatory = $Mandatory.IsPresent
$ParamAttrib.ParameterSetName = $ParameterSetName
$ParamAttrib.ValueFromPipeline = $ValueFromPipeline.IsPresent
$ParamAttrib.ValueFromPipelineByPropertyName = $ValueFromPipelineByPropertyName.IsPresent
$AttribColl.Add($ParamAttrib)
$AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($Param.ValidateSetOptions)))
$RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Param.Name, [string], $AttribColl)
$RuntimeParam
[/powershell]

Does this make sense to you? It didn't to me. It was so non-intuitive it took me awhile to finally wrap my head around it. As I started to Google around I found this great blog article on the Windows Powershell Tips from the Field blog. It was the most concise and explanatory demonstration of dynamic parameters I could find. After using Martin's code and testing with a single parameter it worked great and I sorta understood it. That's great and all but I had 4 parameters I needed to be created dynamically. How could do that? I probably could have just copied/pasted his code 4 times over and over and gotten it to work but I'd have had to kick myself in the nuts if I would taken that shortcut. Bad scripter!

I live and die by the developer principal DRY (don't repeat yourself). I'm anal about that so I decided to see how I could create multiple dynamic parameters using as little code as possible. To do that, what's a common method to "share" code; a function! A function is great when you have a snippet of code that does a single thing that you need to call over and over. This is why I decided to create a New-ValidationDynamicParameter function. Using this function I could easily "replicate" Martin's code over and over to create multiple dynamic parameters.

This is the function I created do that:

[powershell]
function New-ValidationDynamicParam {
[CmdletBinding()]
[OutputType('System.Management.Automation.RuntimeDefinedParameter')]
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Name,
[ValidateNotNullOrEmpty()]
[Parameter(Mandatory)]
[array]$ValidateSetOptions,
[Parameter()]
[switch]$Mandatory = $false,
[Parameter()]
[string]$ParameterSetName = '__AllParameterSets',
[Parameter()]
[switch]$ValueFromPipeline = $false,
[Parameter()]
[switch]$ValueFromPipelineByPropertyName = $false
)
$AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
$ParamAttrib.Mandatory = $Mandatory.IsPresent
$ParamAttrib.ParameterSetName = $ParameterSetName
$ParamAttrib.ValueFromPipeline = $ValueFromPipeline.IsPresent
$ParamAttrib.ValueFromPipelineByPropertyName = $ValueFromPipelineByPropertyName.IsPresent
$AttribColl.Add($ParamAttrib)
$AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($Param.ValidateSetOptions)))
$RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Param.Name, [string], $AttribColl)
$RuntimeParam
}
[/powershell]

To use a function, you need to call a function and pass parameters to it, right? Here's the second iteration of my Set-MyFileSystemAcl function including the dynamic parameters and how I'm calling it.

[powershell]
function Set-MyFileSystemAcl {
[CmdletBinding()]
[OutputType()]
param (
[Parameter(Mandatory)]
[ValidateScript({ Test-Path -Path $_ })]
[string]$Path,
[Parameter(Mandatory)]
[string]$Identity
)
DynamicParam {
$ParamOptions = @(
@{
'Name' = 'Right';
'Mandatory' = $true;
'ValidateSetOptions' = ([System.Security.AccessControl.FileSystemRights]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name
},
@{
'Name' = 'InheritanceFlags';
'Mandatory' = $true;
'ValidateSetOptions' = ([System.Security.AccessControl.InheritanceFlags]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name
},
@{
'Name' = 'PropagationFlags';
'Mandatory' = $true;
'ValidateSetOptions' = ([System.Security.AccessControl.PropagationFlags]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name
},
@{
'Name' = 'Type';
'Mandatory' = $true;
'ValidateSetOptions' = ([System.Security.AccessControl.AccessControlType]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name
}
)
$RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
foreach ($Param in $ParamOptions) {
$RuntimeParam = New-ValidationDynamicParam @Param
$RuntimeParamDic.Add($Param.Name, $RuntimeParam)
}

return $RuntimeParamDic
}

begin {
$PsBoundParameters.GetEnumerator() | foreach { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue'}
}

process {
try {
$Acl = Get-Acl $Path
#$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule('Everyone', 'FullControl', 'ContainerInherit,ObjectInherit', 'NoPropagateInherit', 'Allow')
$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule($Identity, $Right, $InheritanceFlags, $PropagationFlags, $Type)
$Acl.SetAccessRule($Ar)
Set-Acl $Path $Acl
} catch {
Write-Error $_.Exception.Message
}
}
}
[/powershell]

This may seem like it's a little crazy looking and it kinda is. This is way more complicated than just using simple parameters but what's the fun in that?!? Let me break it down a little bit.

In my Set-MyFileSystemAcl function, I had two simple parameters; $Identity and $Path. $Identity is the name of the user or group I'd like to use and the $Path is the path to a file or folder. I've already got built-in tab-completion browsing to a file or folder and I don't really feel like querying my entire Active Directory domain for all the users just so I can tab-complete the user or group. This left me with 4 parameters to be made dynamically; $Right, $InheritanceFlags, $PropagationFlags and $Type. These parameters coincided with the names of the FileSystemAccessRule constructor parameters. Each of these parameters represented it's own .NET object with it's own values that it needed to have.

For example, the right parameter that I needed to have is the $Right parameter. This is the parameter that gives me the option to set values like FullControl, Modify, Read, etc. I don't have all those values memorized so how can I get tab-completion for my $Right parameter. First, where are these values stored? Since they are properties of a .NET object called FileSystemRights they have to be stored in that object somewhere so how do I get to them? This took me awhile and thanks to Trevor (@pcgeek86) I figured it out.

It turns out that you can find all of a .NET object's constructor parameters by doing this:

[powershell]
([System.Security.AccessControl.FileSystemRights]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name
[/powershell]

What's this doing? It's enumerating all of the members of the FileSystemRights .NET object, finding only those that are static properties and outputting them into an array of strings. If you just want to rifle through them now you could just do this:

[powershell]
[System.Security.AccessControl.FileSystemRights]::
[/powershell]

...and start hitting tab. This scrolls through all the static members of that particular .NET object.

Now I had all the possible values for the $Right parameter that I needed in my function line:

[powershell]
$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule($Identity, $Right, $InheritanceFlags, $PropagationFlags, $Type)
[/powershell]

I simply repeated this for the other 3 parameters by putting all the attributes I wanted for each parameter into a hash table nested inside of an array. This way I could loop through each parameter set.

[powershell]
$ParamOptions = @(
@{
'Name' = 'Right';
'Mandatory' = $true;
'ValidateSetOptions' = ([System.Security.AccessControl.FileSystemRights]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name
},
@{
'Name' = 'InheritanceFlags';
'Mandatory' = $true;
'ValidateSetOptions' = ([System.Security.AccessControl.InheritanceFlags]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name
},
@{
'Name' = 'PropagationFlags';
'Mandatory' = $true;
'ValidateSetOptions' = ([System.Security.AccessControl.PropagationFlags]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name
},
@{
'Name' = 'Type';
'Mandatory' = $true;
'ValidateSetOptions' = ([System.Security.AccessControl.AccessControlType]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name
}
)
[/powershell]

Once I had all my dynamic parameters nicely tucked in an array I then was able to call my New-ValidateDynamicParameter function.

[powershell]
$RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
foreach ($Param in $ParamOptions) {
$RuntimeParam = New-ValidationDynamicParam @Param
$RuntimeParamDic.Add($Param.Name, $RuntimeParam)
} return $RuntimeParamDic
[/powershell]

Unfortunately, I had to include some dynamic parameter code inside of my Set-MyFileSystemAcl function simply due to how they are created. In the above snippet, I'm created a dictionary object to store all my parameters into. I'm then dropping down into my array full of parameter attributes and calling my New-ValidationDynamicParam function. This function returns an object type of System.Management.Automation.RuntimeDefinedParameter as you can see from this line in the New-ValidationDynamicParam function:

[powershell]
[OutputType('System.Management.Automation.RuntimeDefinedParameter')]
[/powershell]

This object type is necessary for Powershell to understand the dynamic parameter. I'm not going to go too deep into my New-ValidationDynamicParam function. I hope my coding methodology is self-documenting. I'm simply replicating Martin's code that I found, cleaned it up and functionalized it so I can call it you see in my calling function. Once the dynamic param function returns my RunTimeDefinedParameter object I can now add this to the RuntimeDefinedParameterDictionary object. This is the object type Powershell needs to see in order to properly create the dynamic parameters.

So I've now created all my ValidateSet() sets via dynamic parameters that pull it's values from the constructor members from 4 different .NET objects. We're done, right? Nope! Well..almost. Even though everything is done you still can't use tab-completion in the console. The reason is because the parameters are only stored in the $PSBoundParameters hash table that exists in all advanced functions. You're going to have to do a quick conversion to get a pretty -Right or -InheritanceFlags option in your console. To do this, you need to create those variables from the values inside $PSBoundParameters like this:

[powershell]
$PsBoundParameters.GetEnumerator() | foreach { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue'}
[/powershell]

This just loops through that hash table and creates variable names that are the same as the hash table keys and variable values as hash table values. Be sure to get that ErrorAction param on there since I'm using 2 variables that aren't dynamic parameters and those already exist.

So.....what did this get us? This beautifully simple way to now tab-complete the parameters based on the .NET objects they represent. I now don't have to worry about ever passing a wrong value in and the parameters will always be representative of the .NET object's actual attributes. Untitled