Trying to use parameters dynamically using powershell - powershell

I am trying to setup dynamic parameters that vary depending on if you are adding or modifying/removing a drone. Ex: If you are adding a drone you would need its IP/Name/Location.. To remove the drone you would only need its name. I have tried looking online and try various examples I've seen but I am completely stuck here. Any help to steer me in the right direction would be appreciated. I am somewhat new to powershell. Here's what I have.
[CmdletBinding(SupportsShouldProcess=$True)]
Param( [Parameter(Mandatory=$true,
HelpMessage = "Add remove or Modify a drone?")]
[ValidateSet("Add", "Remove", "Modify")]
[String]$Action)
DynamicParam{
if ($action = "Add"){
Param( [Parameter(Mandatory)]
[ValidateSet("NorthAmerica", "SouthAmerica", "NorthernEurope","UK", "CEE", "FRMALU", "SouthernEurope", "AsiaPacific")]
[String]$curRegion,
[Parameter(Mandatory)]
[IPAddress]$ip,
[Parameter(Mandatory)]
[String]$droneName)
}
if ($action = "Remove"){
Param(
[Parameter(Mandatory)]
[string]$droneRemoveName)
}
}

Consider driving your parameter constraints with named Parameter Sets instead. I'm suggesting this because dynamic parameters don't work quite like you think they do, but named parameter sets are an easier way to solve your problem. In case you're interested, here's a blog post explaining how to use dynamic parameters and it winds up being pretty manual parameter handling.
You can add a parameter to more than one parameter set depending on the contexts in which each parameter is required. Instead of using -Action ACTION as a driver for a dynamic parameter, use a [switch] instead, such as -Add and -Remove, and have each switch part of its own parameter set. For example, when defining your parameters, it may look something like this:
Param(
[Parameter(ParameterSetName='Remove')]
[switch]$Remove,
[Parameter(ParameterSetName='Add')]
[switch]$Add,
[Parameter(ParameterSetName='Remove', Mandatory)]
[Parameter(ParameterSetName='Add', Mandatory)]
[string]$IPAddress
)
In this example, -IPAddress is valid when you use the -Add or -Remove switch, but won't be relavant outside of this context. Of course, if a parameter should only be valid for a certain parameter set, don't define it under more than one parameter set name.
If you want to make sure at least one "action" switch is defined before executing, you can check that one of those parameters was used when invoking the cmdlet by checking $PSBoundParameters:
('Add' -in $PSBoundParameters.Keys) -Or ('Remove' -in $PSBoundParameters.Keys)

Related

Pipeline input not being validated when a function emits no output down the pipeline for ValueFromPipelineByPropertyName parameters

I was able to reproduce this in a more generic way, and the issue is different than originally presented. I have rewritten this question to reflect the issue experienced along with a generic reproducible example.
I have a cmdlet that sometimes produces no output when it doesn't find any data to return. However, I use this function to pass information to another cmdlet which accepts pipeline input via way of the ValueFromPipelineByPropertyName attribute. When there is an actual object being passed down the pipeline, everything works as expected, including parameter validation checks. However, if the passed object is $null, then parameter validation gets skipped. Note that this is not reproduceable when simply passing $null down the pipeline; I've only been able to reproduce this when emitting no output down the pipeline.
I've been able to reproduce this generically. The parameters are defined with the same attributes as my real code:
Function Get-InfoTest {
Param(
[switch]$ReturnNothing
)
if( !$ReturnNothing ) {
[PSCustomObject]#{
Name = 'Bender'
Age = [int]::MaxValue
}
}
}
Function Invoke-InfoTest {
Param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]$Name,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[int]$Age
)
Write-Host "Hello, $Name. I see you are $Age years old."
}
# With valid object
Get-InfoTest | Invoke-InfoTest
# Correct behavior when $null is directly passed into the cmdlet, throws error
$null | Invoke-InfoTest
# With returned null object, should throw an error but executes with an incorrect result
Get-InfoTest -ReturnNothing | Invoke-InfoTest
What is going on here? While it's not difficult to write null-or-whitespace checks in the function body, this is the point of the Mandatory parameter option as well as the Validate* parameter attributes. In my real code, I now need to write null-or-whitespace checks for several parameters which already have validation attributes set. As stated in the code comments, passing $null into the target cmdlet results in the correct error being thrown, but no output produced from a function results in the function executing as if everything was provided correctly.
If you don't define begin/process/end blocks, functions bodies default to an end block. However, putting the function body in an explicit process block results in the correct behavior:
The following modification to Invoke-InfoTest results in the sample code working correctly for all cases:
Function Invoke-InfoTest {
Param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]$Name,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[int]$Age
)
# Note that I've wrapped this in a process block
process {
Write-Host "Hello, $Name. I see you are $Age years old."
}
}
This works because as stated above, functions default to an end block if unspecified. However, the end and begin blocks are executed regardless of the pipeline object being input. process only gets executed when there is data passed in. Defining the code using the pipeline variables inside of a process block effectively stops the code using the missing data from being executed, which seems to be by design.
Thanks to #SantiagoSquarzon in the comments for helping me realize the actual problem.

Parameter set cannot be resolved with mutually exclusive non default parameters

Following is full param block signature, I tried almost every possible solution that I'm aware of such as
adding Mandatory to File and Thumbprint parameters sets, removing default parameter set etc, none of which works.
Problem description and desired functionality is this:
Domain parameter is always required while File and ThumbPrint are optional but mutually exclusive.
I run this test as follows:
.\Set-WinRM* -Domain VM-PRO
Parameter set cannot be resolved using the specified named parameters.
function Set-WinRM
{
[CmdletBinding(PositionalBinding = $false, DefaultParameterSetName = "None")]
param (
[Parameter()]
[ValidateSet("HTTP", "HTTPS", "Any")]
[string] $Protocol = "HTTPS",
[Parameter(Mandatory = $true)]
[Alias("ComputerName", "CN")]
[string] $Domain,
[Parameter(ParameterSetName = "File")]
[string] $CertFile,
[Parameter(ParameterSetName = "ThumbPrint")]
[string] $CertThumbPrint,
[Parameter()]
[switch] $SkipTestConnection,
[Parameter()]
[switch] $ShowConfig
)
}
EDIT:
I never use dynamic parameters, if this can't be done normally maybe you can provide an example on how to define them in this example, that would be great.
I copy and pasted your code into Powershell ISE, and added echo $domain to the end to test the parameter. It returns the value normally, without errors:
I don't see any issues with your parameter block, which leads me to believe something else is at fault. If you type out .\Set-WinRM.ps1 -Do, or Set-WinRM -Do can you tab-complete it successfully? If you run Set-WinRM without parameters at all, does it prompt you for Domain:?
I would only expect to see that error if you had additional parameter sets with $domain doing different things, or maybe if a module you have loaded has the Set-WinRM command and it's taking precedence. Try Get-Command Set-WinRM and make sure the CommandType is Function and the Source is blank.

How to accept either a "live" object or a deserialized object of the same type in a param block? [duplicate]

This question already has an answer here:
Can a PowerShell function handle multiple input types?
(1 answer)
Closed 1 year ago.
I have a script that deals with Active Directory User objects (Microsoft.ActiveDirectory.Management.ADUser). I explicitly list the type in the function that processes these objects:
function Write-ADUser {
param (
[Microsoft.ActiveDirectory.Management.ADUser]$user
)
(...)
I also want this function to be able to take objects from remote sessions. The challenge is that objects returned from remote sessions are of the deserialized variety:
C:\> icm -session $sess { get-aduser -identity testuser -credential $cred } | gm
TypeName: Deserialized.Microsoft.ActiveDirectory.Management.ADUser
Is there a way to have my function param block accept either the "live" object or the deserialized variant? My function doesn't need to use methods - the deserialized variant has (or can be made to have) what I need.
The parameter sets idea was interesting and a helpful lead. After reviewing the documentation, this is the best option I could come up with:
function Write-ADUser {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
[ValidateScript({
if ($_.GetType().Name -notin #('ADUser', 'PSObject')) {
throw ("Error: Invalid type ````{0}'' - expecting ADUser.") -f $_.GetType().Name
} else {
$true
}
})]
$user
)
...
One other comment. When looking into parameter sets I kept getting an error about ADUser. However, upon further digging I believe that error is because the Microsoft Active Directory PowerShell module isn't installed on my test computer. Therefore, the 'ADUser' type isn't defined. Because I want this script to run on computers that don't necessarily have the ADModule I am using the above logic. However, if I could guarantee that ADModule was present then I think parameter sets would be the way to go.
Apologies for not providing clearer requirements. I'm still learning PowerShell...
Note - updated based on feedback from #zett42

Is there a way to make a Powershell function ignore a default parameter's value if its parameter set is not in use?

I understand from this answer that when you have default parameters and multiple parameter sets in a Powershell function, the default parameter values will be instantiated even if the parameter set in use is not the one in which they are inserted.
Is there a way to avoid this?
For example, in the function below, assuming that there is a really expensive calculation used to compute the default value of $FirstParameter, I would like to avoid using it when it is not necessary:
function PrintStuff {
[CmdletBinding(DefaultParameterSetName='FirstSet')]
Param(
[Parameter(ParameterSetName='FirstSet')]
[String]$FirstParameter=(ReallyExpensiveFunction),
[Parameter(ParameterSetName='SecondSet')]
[String]$SecondParameter
)
if (-not ($FirstParameter -eq $null)) {Write-Host $FirstParameter}
Write-Host "$($PSCmdlet.ParameterSetName)"
}
function ReallyExpensiveFunction {
# Very expensive calculation
"I Am First"
}
However, at the moment running it would still give me the results below:
PS C:\> PrintStuff
# I Am First
# FirstSet
PS C:\> PrintStuff -SecondParameter "don't print this"
# I Am First
# SecondSet
As per above, when SecondSet is used $FirstParameter is still being defined. Is there a way to get only SecondSet printed when the second parameter set is used?
Bear in mind, I am looking to find out if there is a solution which would allow me to keep ReallyExpensiveFunction as the default value for $FirstParameter, and avoid solutions which would involve transferring the logic to the body of the function, such as:
...
Param(
[Parameter(ParameterSetName='FirstSet')]
[String]$FirstParameter,
...
)
if ($PSCmdlet.ParameterSetName -eq 'FirstSet' -and ($FirstParameter -eq '')) {
$FirstParameter = ReallyExpensiveFunction
}
...
Sorry if the pitch is too specific, but I am curious to find out if this is possible.
Unfortunately, the answer is no. ParameterSet allows to present a simpler interface to user for complex argument sets by filtering out the non-relevant ones. However, PowerShell goes through each parameter, whether it is in the selected parameterset or not and assign the default value to the parameter, if you specify one. So, simply put in the context of your question, ParameterSet may be thought as just a filter for presentation.

Parameter, if switch is present, don't permit other parameters

I have a function with parameters like this:
param(
[string[]]$ComputerName,
[string]$exepath,
[string[]]$exeargs,
[switch]$help
)
If the User who uses this function uses the -help switch, there must not be any other parameters declared to call the function. I want to prevent anyone using -help alongside other parameters.
How can I achieve that? I read about_Functions_Advanced_Parameters but it doesn't really help me. I first thought what I need is a ValidateSet but that's different from what I need.
You could use the ParameterSetName attribute:
param(
[Parameter(ParameterSetName='default')]
[string[]]$ComputerName,
[Parameter(ParameterSetName='default')]
[string]$exepath,
[Parameter(ParameterSetName='default')]
[string[]]$exeargs,
[Parameter(ParameterSetName='help')]
[switch]$help
)