Other scripts with dynamic parameters have never given me any issues. The one thing that is different is this is the first time I am using a SecureString type on a static parameter.
If I call this paramenter, the Dynamic one will not appear. I'm not doing anything special to hide the dynamic parameter. It should always appear. The issue appears on both 5.1 and PowerShell core.
I would be interested to know if there is a solution to this problem.
The dynamic parameter looks at available system services and allows you to enter only those services as part of the parameter.
This is only an example to replicate what I'm experiencing. Calling the Name parameter and supplying the information has no effect on the Service Dynamic Parameter. As soon as Password Parameter is called, you cannot call the Service Parameter if you hadn't already.
function test-command {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
$Name,
[Parameter(Mandatory)]
[SecureString]
$Password
)
DynamicParam {
$ParameterName = 'Service'
$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
$ParameterAttribute.HelpMessage = "Parameter Help."
$AttributeCollection.Add($ParameterAttribute)
$arrSet = (Get-Service).Name
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
$AttributeCollection.Add($ValidateSetAttribute)
$AttributeAlias = New-Object System.Management.Automation.AliasAttribute('s', 'Serv')
$AttributeCollection.Add($AttributeAlias)
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [array], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
return $RuntimeParameterDictionary
}
begin {
$Service = $PsBoundParameters[$ParameterName]
}
process {
$Service
}
end {
}
}
Yes, I could call Service Parameter first and then the remaining ones, but if there is an answer as to why it's behaving this way I'd certainly like to know. Plus, if someone else uses my script, they wouldn't automatically know to do this. Is this a bug or am I doing something wrong?
A Module Variable
$ModVar = #{Value = Key}
Errors when using static Param -Name, as a key reference.
PS> help Test-Function
InvalidOperation:
Line |
214 | $ModItem = $ModVar[$Name]
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Index operation failed; the array index evaluated to null.
I believe this is because the Param -Name does not exist until execution time. The function otherwise works correctly when used, this error only happens when using Get-Help.
Is there a methodology for getting around the error?
I have tried:
If (!$Name) {$Name = "A_Key_in_ModVar"}
In an attempt to supply a dummy value when it doesn't detect a value in -Name, but this causes the function to always overwrite the value of -Name, even if the parameter -Name is provided with an explicit value from the terminal.
Gist Example: GitHub to Get-KeyAndPeeleSchoolName
Code Snippet (full example above):
DynamicParam {
$ParameterName = 'School'
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
Write-Error $Name
$attSet = $Table[$Name]
Write-Error $attSet
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($attSet)
$ValidateSetAttribute.ErrorMessage = "Value must be $attSet"
$AttributeCollection.Add($ValidateSetAttribute)
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.ParameterSetName = "ParamSet1"
$AttributeCollection.Add($ParameterAttribute)
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
$RuntimeParameterDictionary
}
Edit 1: Other issues I noticed.
Values for -Name that contains spaces prevent the Dynamic Param block containing -School from working.
Also when using ArgumentCompleter on -Names, the possible values with complex punction fail to completely autocomplete. This is probably because of the matching behavior, but any suggestions for how to make this behave would be great.
Also using " " quotes around a parameter value also prevent the argument completer from working, any suggestions for this would also be great.
Wrapping the [RuntimeDefinedParameterDictionary] object and accompanying constructs in an IF statement resolved the issue. -Name does not exist pre-binding when -Name is not passed a value explicitly. It is also not available when Get-Help runs to analyze the makeup of the Function.
To resolve this, check for the existence of the parameter being referenced within the DynamicParam block to control its execution.
DynamicParam {
if ($Name) { #If $Name exist create Dynamic Param.
$ParameterName = 'School'
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$attSet = $Table[$Name]
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($attSet)
$ValidateSetAttribute.ErrorMessage = "Value must be $attSet"
$AttributeCollection.Add($ValidateSetAttribute)
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.ParameterSetName = "ParamSet1"
$AttributeCollection.Add($ParameterAttribute)
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
$RuntimeParameterDictionary
}
}
As a Powershell learner, I ran into -f format operator.
I then thought to myself, is it possible to dynamically change code based on a condition. Take for example a mapping drive scenario:
$creds = Get-Credential
$share_needs_creds = $true
$drive_map = New-Object -ComObject WScript.Network
if ($share_needs_creds){
$drive_map.MapNetworkDrive('Z:', '\\server\share', $false, "$($creds.Username)", $($creds.GetNetworkCredential().Password)")
} else {
$drive_map.MapNetworkDrive('Z:', '\\server\share', $false)
}
can this be re-written as follows:
$creds = Get-Credential
$share_needs_creds = $true
$drive_map = New-Object -ComObject WScript.Network
if ($share_needs_creds){
$condition_smart = ', "$($creds.Username)", $($creds.GetNetworkCredential().Password)"'
} else {
$condition_smart = ''
}
$drive_map.MapNetworkDrive('Z:', '\\server\share', $false{0}) -f $condition_smart
Any advise is appreciated!
That's not going to work - the -f operator returns exactly 1 string as output - and you need to pass a variable number of arguments to the parameter list of the method you're calling.
If you want to invoke a method dynamically on a ComObject, prepare your argument list as an array, and pass it to .Invoke() on the method name exposed by PowerShell's type adapter:
$arguments = #(
'Z:', '\\server\share', $false
if($share_needs_creds){
$creds.Username, $creds.GetNetworkCredential().Password
}
)
$drive_map.MapNetworkDrive.Invoke($arguments)
I found this method of dynamically updating the validate set members of a parameter.
This let's me do something like this:
Function MyFunction([ValidateSet("Placeholder")]$Param1) { "$Param1" }
Update-ValidateSet -Command (Get-Command MyFunction) -ParameterName "Param1" -NewSet #("red","green")
But is there any way of adding a validation attribute that was not already present? Specifically, I have a set of functions that would benefit greatly by having dynamically created validate sets. However, as the link above makes clear, this is a hack, and may break in the future. So I don't want to put a placeholder ValidateSet, in case it needs to be removed in the future. Essentially, I'd like to do something like this:
Function MyFunction($Param1) { "Param1" }
Add-ValidateSet -Command (Get-Command MyFunction) -ParameterName "Param1" -NewSet #("red", "green")
This way, if it ever does break, it would be easier to remove the breaking code. But I have not been able to get this to work. I've tried doing this:
$parameter = (Get-Command MyFunction).Parameters["P1"]
$set = "Red","Orange","Yellow","Green","Blue","Indigo","Violet"
$Attribute = new-object System.Management.Automation.ValidateSetAttribute $Set
$ValidValuesField = [System.Management.Automation.ValidateSetAttribute].GetField("validValues", [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance)
$ValidValuesField.SetValue($Attribute, [string[]]$Set)
$parameter.Attributes.Add($Attribute)
But it does not work.
(Get-Command MyFunction).Parameters["P1"].Attributes
shows that the ValidateSet has been added, but tab completion does not work. Comparing it with the results of using the Update-ValidateSet function, it appears that the difference is that the attribute should also appear under
(Get-Command MyFunction).ParameterSets[0].Parameters[0].Attributes
However, that is a ReadOnlyCollection, so I don't seem to be able to add it there. Am I just barking up the wrong tree here? Is this not possible to do?
You accomplish this using dynamic parameters. The dynamic parameters will be evaluated as your command is typed into the command window.
This is from about_Functions_Advanced_Parameters
function Get-Sample {
[CmdletBinding()]
Param ([String]$Name, [String]$Path)
DynamicParam
{
if ($path -match ".*HKLM.*:")
{
$attributes = new-object System.Management.Automation.ParameterAttribute
$attributes.ParameterSetName = "__AllParameterSets"
$attributes.Mandatory = $false
$attributeCollection = new-object `
-Type System.Collections.ObjectModel.Collection[System.Attribute]
$attributeCollection.Add($attributes)
$dynParam1 = new-object `
-Type System.Management.Automation.RuntimeDefinedParameter("dp1", [Int32], $attributeCollection)
$paramDictionary = new-object `
-Type System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add("dp1", $dynParam1)
return $paramDictionary
}
}
I tried to add System.ComponentModel.DefaultValueAttribute to AttributeCollection of RuntimeDefinedParameter, but it does't work..
Chrissy's example is probably the correct way to do it, but I was unable to retrieve the default value. The parameter does not exist in $PSBoundParameters when default value is specified.
The "workaround" we applied was to bind $PSBoundParameter["Background"] to the value we want as default. $PSBoundParameters["Background"] = "Transparent"
Extending Chrissy's example:
DynamicParam {
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$colorlist = [System.Enum]::GetNames([System.Drawing.KnownColor])
$attributes = New-Object System.Management.Automation.ParameterAttribute
$attributes.ParameterSetName = "__AllParameterSets"
$attributes.Mandatory = $false
# Background color
$validationset = New-Object -Type System.Management.Automation.ValidateSetAttribute -ArgumentList $colorlist
$collection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
$collection.Add($attributes)
$collection.Add($validationset)
$background = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Background", [String], $collection)
$PSBoundParameters["Background"] = "Transparent"
$newparams = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$newparams.Add("Background", $background)
return $newparams
}
As Bartek suggested, the Value property can be used as seen in the code below
DynamicParam {
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$colorlist = [System.Enum]::GetNames([System.Drawing.KnownColor])
$attributes = New-Object System.Management.Automation.ParameterAttribute
$attributes.ParameterSetName = "__AllParameterSets"
$attributes.Mandatory = $false
# Background color
$validationset = New-Object -Type System.Management.Automation.ValidateSetAttribute -ArgumentList $colorlist
$collection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
$collection.Add($attributes)
$collection.Add($validationset)
$background = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Background", [String], $collection)
$background.Value = "Transparent"
$newparams = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$newparams.Add("Background", $background)
return $newparams
}
The important line here is $background.Value = "Transparent" where $background is a RunTimeDefinedParameter.
For those that are curious. I initial tried to use it as an attribute, but there is no .Value available within ParameterAttributes.
UPDATE:
I found an error in my PowerShell function, Test-DynamicParameter, which led to an erroneous conclusion. Below, in the PowerShell function, I will comment out the erroneous line. Also, the very last example output has changed. Again, I will annotate the changes.
OK, so everyone who has answered is correct in how to set the "default" value of a dynamic parameter. (Spoiler alert / tl;dr: there's no such thing.) Now, let me preface my answer by saying that the function I'm about to demonstrate was executed via PowerShell 5.1. Below is what I found in my testing.
First, the function I'm using to test dynamic parameters and default values:
function Test-DynamicParameter {
[CmdletBinding()]
Param (
[switch]$UseDynamicParameterDefault2000
)
dynamicparam {
# $RequestTimeout parameter
$attributeCollection = New-Object Collections.ObjectModel.Collection[Attribute]
$attributeCollection.Add((New-Object Management.Automation.ParameterAttribute -Property #{ ParameterSetName = "__AllParameterSets" }))
$attributeCollection.Add((New-Object Management.Automation.ValidateScriptAttribute { $_ -ge 0 }))
$RequestTimeoutParameter = New-Object Management.Automation.RuntimeDefinedParameter RequestTimeout, int, $attributeCollection
# This line uses an incorrect name for the dynamic parameter, which caused
# incorrect results in my original post. The corrected line
# appears below this commented out, incorrect line.
#$RequestTimeoutParameter.Value = $(if ($PSBoundParameters.UseDynamicParameterDefault1) { 2000 } else { 120000 })
$RequestTimeoutParameter.Value = $(if ($PSBoundParameters.UseDynamicParameterDefault2000) { 2000 } else { 120000 })
$dynamicParams = New-Object Management.Automation.RuntimeDefinedParameterDictionary
$dynamicParams.Add('RequestTimeout', $RequestTimeOutParameter)
$dynamicParams
}
process {
$RequestTimeoutParameter | Format-List
if ($PSBoundParameters.ContainsKey('UseDynamicParameterDefault2000')) {
Write-Host "`$PSBoundParameters contains 'RequestTimeout'? $($PSBoundParameters.ContainsKey('RequestTimeout'))"
Write-Host "`$RequestTimeout default: 2000"
if ($PSBoundParameters.ContainsKey('RequestTimeout')) {
# UPDATE: The following line should have used $PSBoundParameters to access the RequestTimeout parameter and has been corrected below
#Write-Host "`$RequestTimeout = $RequestTimeout (Bound)"
Write-Host "`$RequestTimeout = $($PSBoundParameters['RequestTimeout']) (Bound)"
} else {
# UPDATE: To be safe, also access $RequestTimeout here via $PSBoundParameters
#Write-Host "`$RequestTimeout = $RequestTimeout (Default Value)"
Write-Host "`$RequestTimeout = $($PSBoundParameters['RequestTimeout']) (Default Value)"
}
} else {
Write-Host "`$PSBoundParameters contains 'RequestTimeout'? $($PSBoundParameters.ContainsKey('RequestTimeout'))"
Write-Host "`$RequestTimeout default: 120000"
if ($PSBoundParameters.ContainsKey('RequestTimeout')) {
Write-Host "`$RequestTimeout = $($PSBoundParameters['RequestTimeout']) (Bound)"
} else {
# UPDATE: Again, use $PSBoundParameters
#Write-Host "`$RequestTimeout = $RequestTimeout (UnBound, Default Value)"
Write-Host "`$RequestTimeout = $($PSBoundParameters['RequestTimeout']) (UnBound, Default Value)"
}
}
}
}
And now some tests executing the function (again, using PowerShell 5.1):
PS C:\> Test-DynamicParameter
Name : RequestTimeout
ParameterType : System.Int32
Value : 120000
IsSet : True
Attributes : {__AllParameterSets, System.Management.Automation.ValidateScriptAttribute}
$PSBoundParameters contains 'RequestTimeout'? False
$RequestTimeout default: 120000
$RequestTimeout = (Unbound, Default Value)
PS C:\> Test-DynamicParameter -RequestTimeout 3000
Name : RequestTimeout
ParameterType : System.Int32
Value : 3000
IsSet : True
Attributes : {__AllParameterSets, System.Management.Automation.ValidateScriptAttribute}
$PSBoundParameters contains 'RequestTimeout'? True
$RequestTimeout default: 120000
$RequestTimeout = 3000 (Bound)
PS C:\> Test-DynamicParameter -UseDynamicParameterDefault2000
Name : RequestTimeout
ParameterType : System.Int32
### UPDATE: Due to incorrect code, this line was wrong...
### Value : 120000
Value : 2000
IsSet : True
Attributes : {__AllParameterSets, System.Management.Automation.ValidateScriptAttribute}
$PSBoundParameters contains 'RequestTimeout'? False
$RequestTimeout default: 2000
$RequestTimeout = (Default Value)
PS C:\> Test-DynamicParameter -UseDynamicParameterDefault2000 -RequestTimeout 3000
Name : RequestTimeout
ParameterType : System.Int32
Value : 3000
IsSet : True
Attributes : {__AllParameterSets, System.Management.Automation.ValidateScriptAttribute}
$PSBoundParameters contains 'RequestTimeout'? True
$RequestTimeout default: 2000
### UPDATE: This line is incorrect when the PowerShell function is corrected.
### $RequestTimeout = 3000 (Bound)
###
$RequestTimeout = 3000 (Bound)
PS C:\>
OK, so I learned a few things from this output. One is that I was trying to use $PSBoundParameters while constructing the dynamic parameter in order to set its default value (to either 2000, or 120000). However, this doesn't work, as the parameters have not yet been processed. (I was wrong, you can use $PSBoundParameters while constructing dynamic parameters.) What happens is the parameter is created, then the values sent into the cmdlet for the various parameters are bound. In the case where a value for the dynamic parameter is specified, the dynamic parameter's Value property is updated. In this sense, then, the Value property is not a default value for the dynamic parameter; it's the value for the parameter.
So, in my function where I try to set the "default" value of the dynamic parameter contingent upon the value of other (bound) parameters, this doesn't work and the value 120000 is always set as the initial value for the dynamic parameter. (Due to my coding error, this was true. But once the code is corrected, this statement is false.)
OK, but when I don't specify -RequestTimeout <n> to an invocation of the cmdlet, referring to $RequestTimeout in the cmdlet results in a null value. What gives? How do I get the default value I set on the parameter? That's easy. I still have access to the $RequestTimeoutParameter variable I used to build up the parameter definition. And as you can see in the output, I wrote that out and you can see the Value property is set. Furthermore, when -RequestTimeout <n> is specified, the $RequestTimeoutParameter.Value property is updated with the value passed in from the command invocation.
I hope this helps someone else out there.
System.Management.Automation.RuntimeDefinedParameter has 'Value' property, so I guess I would use it to set default value. It sort of works (when I debug script I can see this 'default' value using $PSCmdlet.GetDynamicParameters() ) but I had no luck accessing it in actual function (it's execution 'died' on $pscmdlet.GetDynamicParameters() call).
Anyway: when I specified value my function was showing bound value rather than default one.
Not sure if it helps, and TBH I hardly see any use case for default value when parameter is dynamic. Would love to see why you need it. :)
It is not exactly what you need, but at least a good workaround (requires PowerShell 3.0):
You can set the default values for all parameters of all functions using $PSDefaultParameterValues. See https://technet.microsoft.com/en-us/library/hh847819.aspx or use PowerShell help on about_Parameters_Default_Values.
This might be obvious, but I didn't see anyone mention this specifically yet, but if you combine fourpastmidnights excellent explanation, chrissy's initial answer, and Tomas's clever suggestion.
You can assign the value of the Dynamic Param, which changes based on user provided input, to the $PSBoundParameter Dictionary dynamically, achieving the desired effect of providing an accesible default value when nothing is specified, but also updating it in the event user input is provided.
dynamicParam {
if ($Param1 -eq $True) {
#Create Abstract Object
$paramDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
$attributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
#Define Param attributes
$attribute = New-Object System.Management.Automation.ParameterAttribute
$attribute.Mandatory = $false
$attributeCollection.Add($attribute)
#Create Param
$Name = 'DynamicParam1'
$dynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter($Name, [bool], $attributeCollection)
$dynParam.value = $true
$paramDictionary.Add($Name, $dynParam)
$PSBoundParameters["$name"] = $dynParam.Value
}
$paramDictionary
}
Thus this example will then let you access the value of $PSBoundParameters.DynamicParam1 for either the default value $true, or the user provided value if one was supplied.
From what I can tell the line $PSBoundParameters["$name"] = $dynParam.Value must be after the "default" value assignment statement $dynParam.value = $true.
This was tested on PS 7.1
Why not check if the parameter was specified by the user and if not, provide a value for the parameter in the begin-block for instance;
function Get-Something {
param()
dynamicparam{
<Definition of dynamic parameter "ThisIsADynamicParam">
}
begin {
if (-not $PSBoundParameters.ContainsKey('ThisIsADynamicParam') {
$PSBoundParameters.Add('ThisIsADynamicParam','DefaultValue')
}
}
}