Today I would like to share “creative” way of abusing dynamic parameters in PowerShell I came up with yesterday. All started with challenging question on PowerShell TechNet forum. But before I get there I would like to show also why and how you would use them normally, so that you know why I would call my trick an “abuse”. All examples will be pretty naive and not production-oriented. For better examples – lookup poshcode, especially Joel’s and Oisin’s magical tools. Few to start with:
Why?
If you want your command to cover several similar yet slightly different scenarios, than most of time you could get away with ParameterSets. At times, however, you would have to come up with some bizarre parameters to cover everything. E.g. you want your command to give option to –Capitalize the name but only if the –Name contains lower chars at word boundary. Obviously, I can have –NameStartingWithoutUpperCase and –NameStartingWithUpperCase, one in parameter set with and other without –Capitalize switch, but it’s… ugly. Dynamic parameters give us option to have a “peek” at our $Name and decide – give user option to –Capitalize or not. So it gives us flexibility to add parameters when needed.
How?
That is actually pretty tough question. I could not get my head around this concept and even reading Joel’s and Oisin’s examples was not helping much until yesterday, when I decided I have to start using it. Probably it was too develop-ish and .NET-ish for me.
First you may want to read documentation. Enough for you to understand whole concept? Well, than I’m impressed. For me it was not obvious enough so I mainly did a try and error thing to finally get my solution working. But first: our slightly more “expected” scenario. Here is how my function would look like with dynamic parameters used instead of parameter sets with some validation and crazy names:
function Get-Greeting { [CmdletBinding()] param ( $Name ) DynamicParam { if ($Name -cmatch '\b[a-z]') { # For blog width... # As you can see - also abusing splatting. # Here - to pass positional parameter TypeName $Type = @( 'Management.Automation.ParameterAttribute' ) $Attributes = New-Object @Type $Attributes.ParameterSetName = "__AllParameterSets" $Attributes.Mandatory = $false $Type = @( 'Collections.ObjectModel.Collection[Attribute]' ) $AttributeCollection = New-Object @Type $AttributeCollection.Add($Attributes) $MyParameter = @{ TypeName = 'Management.Automation.RuntimeDefinedParameter' ArgumentList = @( 'Capitalize' <# name #> [switch] <# ParameterType #> , $AttributeCollection <# Attributes #> ) } $Dynamic = New-Object @MyParameter $Type = @( 'Management.Automation.RuntimeDefinedParameterDictionary' ) $ParamDictionary = New-Object @Type $ParamDictionary.Add("Capitalize", $Dynamic) $ParamDictionary } } end { if($PSBoundParameters.Capitalize) { $Name = [regex]::Replace( $Name, '\b\w', { $args[0].Value.ToUpper() } ) } "Welcome $Name!" } }
One thing to remember: if you add DynamicParam you have to have begin/process/end to wrap your code. Otherwise it won’t work. Either DynamicParam will be seen as command (and in effect – fail) or PowerShell will complain about curly braces. Here I’ve used ‘end’ block, because I do not plan to use pipeline at all. If you run this function and pass name that have some lower case letters where those probably should not be – you will get an option to –Capitalize them. So both will produce same results, with last one giving you error instead:
# Work as expected... Get-Greeting -Name Bartek Get-Greeting -Name bartek -Capitalize # Fails with "Parameter not found" Get-Greeting -Name Bartek -Capitalize
Tab will work with it most of time, unless it won’t be able to tell quickly if parameter should be there or not. In such a case it will assume not.
Why abuse?
Question on the forum was simple: is it possible to run code before param block. Or rather: how to run code to avoid issues with fixing parameter type when this type may not be loaded before function is executed. OP was trying to use ValidateScript to do that (I would also probably start there) but it did not help. With dynamic parameters that is no-issue. You can run pretty long script before you actually start binding anything to the parameter of your choice. If you force it to be always available – tab will pick it up in any possible scenario. The sky is the limit. My original code was for creating Enum with Add-Type required Enum was not there yet. Having parameter that is an enum may be handy at times, but if you want to have custom one – you need to add it first. If you asking yourself – why do it inside function – well, to be honest I have no idea. But I’m not to judge, I’m to code. My take would probably include module. I have tendency to make almost everything a module to simplify and isolate things. But OP wanted to have all in one, black box, so let’s do it.
How to get there?
Well, all we need to do is modify our previous function a little bit. We have different parameter name, no condition to check (in forum post I’ve used if ($true) but it’s not necessary), and we want to run small try-catch to add-type if it’s not there yet:
DynamicParam { try { New-Object MyEnum | Out-Null } catch { Add-Type 'using System; public enum MyEnum { foo, bar }' } # ... same code... with some changes here: $MyParameter = @{ TypeName = 'Management.Automation.RuntimeDefinedParameter' ArgumentList = @( 'Enum' <# name #> [MyEnum] <# ParameterType #> , $AttributeCollection <# Attributes #> ) } # ... and here: $ParamDictionary.Add("Enum", $Dynamic) $ParamDictionary } end { $PSBoundParameters.Enum $PSBoundParameters.Enum.GetType().FullName }
The result is exactly what OP wanted (I hope ):
It’s not something you could not live without. But for me it was good reason to finally try to use DynamicParam(s). And I can see few situations when it would actually make sense in production-oriented scripts. The only danger now is that I will use them far too often.
How to create Multi-dynamic parameters.
If your question is: how to create more than one dynamic parameter than answer would be: just create more parameters (using class RuntimeDefinedParameter) and add all of them to RuntimeDefinedParameterDictionary, that you will return as the last step in DynamicParam scriptblock. This is a collection, so you should be able to add as many parameters as you need, obviously – each can be defined/ added based on different condition.
I’ll narrow down the question by showing the example:
-WriteToDB switch is present -> add SQLServer,DBname parameters
-NotifyUsers switch is present -> add SMTPServer parameter
How can I accomplish this?
Hi there. I’m struggling with Dynamic Params today, I’ve written a script (Posh4 on 2012r2) for building a new VM in Hyper-V which looks up the names of the possible values as you go. E.g. I have a parameter -HWProfile which dynamically looks up the Name values returned from Get-SCHardwareProfile.
My problem is that many of these values have spaces in them which break the command!
I have tried putting quotes around each value in the set before it is passed to the function which creates the Dyanmic Params, but then the command fails as the quotes are stripped from the string when it is passed in as a parameter!
Any idea how to around this issue? Happy to send through the full script.
That’s actually bug in the way TabExpansion works for ValidateSets. Even if you create one by hand:
… it will not work as it should.
It’s funny that you mention it though: at PowerShell Summit Europe I was presenting this topic and this use case for dynamic parameters. Knowing this limitation would fit perfectly in “why would you *not* use dynamic parameters for that”. Alternative? TabExpansion++. It would let you do what you want for very low cost. You can grab it here: https://github.com/lzybkr/TabExpansionPlusPlus. It saves you from creating wrapper, dynamic adding validateSets (that fail) and gives you option to easily add TabExpansion experience that ‘just works’ for any dynamic data on your parameters.