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

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.

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.

Should Whatif and ConfirmImpact have an else clause?

I want to include Whatif and Confirm to my functions but I encountered an issue with these parameters.
My functions are structured like this:
function Get-Stuff {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param ( {...} )
process {
if ($PSCmdlet.ShouldProcess($Name, "Delete user")) {
$result = Invoke-RestMethod #restBody
}
}
end {
Write-Output -InputObject $result
Remove-Variable -Name result
}
}
I took on a habit to clean up my variables in the end-block with Remove-Variable. When I use now the -WhatIf or the -Confirm parameter (and denying it), I get an error that the $result variable is null.
ErrorRecord : Cannot find a variable with the name 'result'.
I understand that the RestMethod is skipped in this case but I would assume that the rest of the function would not be executed further.
My question is now, does one add an else-clause to end the continuing execution of the function or do I use these parameters incorrectly?
There's no good reason to remove your variables in the end block, since they go out of scope automatically anyway, given that they're local to your function.
(The only thing that makes sense is to .Dispose() of variables containing objects that implement the System.IDisposable interface; if releasing memory as quickly as possible is paramount - at the expense of blocking execution temporarily - you can additionally call [GC]::Collect(); [GC]::WaitForPendingFinalizers())
If you still want to call Remove-Variable, you have two options:
Simply ignore a non-existent variable by adding -ErrorAction Ignore to the Remove-Variable call.
Remove-Variable -Name result -ErrorAction Ignore
Alternatively, protect the call - and the Write-Output object - with an explicit test for the existence of the variable:
if (Get-Variable -Scope Local result -ErrorAction Ignore) {
$result # short for: Write-Output -InputObject
Remove-Variable -Name result
}
Also note that it's typical for output objects to be emitted directly from the process block - emitting from the end block is only a necessity for commands that for conceptual reasons must collect all input first, such as Sort-Object.
Emitting output objects from the process block - which is invoked for each input object - ensures the streaming output behavior - emitting objects one by one, as soon as they're available - that the pipeline was designed for.

Trying to use parameters dynamically using 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)

Powershell Parameter passing issue

I have a strange one I have searched the existing Q&A and haven't found a match.
I have written my functions using parameter validation using the basic format
function FunctioName
{
[CmdletBinding()]
Param(
[parameter(Mandatory)]
[String]$VariableName
)
When I set the parameter to Mandatory as above I get a parameter binding exception indicating a null value was passed. Running the script in debug I can see the function parameter being passed is not null and is a valid string.
When I run the script in the exact same way without the mandatory flag the string is passed into the function and it executes correctly.
Has anyone got any ideas, what could be the issue. This problem is affecting a number of functions in my application interestingly it appears that the affected functions all have only a single parameter functions with multiple parameters do not appear to be affected.
Ok thanks guys for your feedback its much appreciated. BTW i am using powershell 5 .
Further to the issue, looking into it further I found that the variable was being passed to the function as an array of strings, however an empty string value was being appended into the array which I believe was the cause for the issue. This is where it starts to get interesting, I will need to give a bit more background.
The script I am running queries active directory for user attributes meeting specific conditions, those that match I create an array of strings with each value a delimited value of the user,hostname and other attribute properties.
To ensure that I am getting the latest values I use the ASDI GetInfo method,which seems to trigger the odd behavior.
At a high level the functions are
Function GetuserAttr
{
$inscopeusers = New-Object System.Collections.ArrayList
$accountlist = (Get-ADUser -Filter { attribute1 -eq "value"} -Properties attribute1).SamAccountName
foreach ($user in $accountlist)
{
$DN = getDN($user) # basically a funtion I wrote to create ASDI object for user account.
$DN.GetInfo() # this method call appears to cause issues
$attr1 = $DN.Get("Attribute1")
$attr2 = $DN.Get("Attribute2")
$hoststring = "$($user)|$($attr1)|$($attr2)"
$inscopeusers.Add($hoststring) > null
}
return $inscopeusers
}
The string array returned in this function is fed into a number of other functions, one of which is the one that was giving the error that I originally brought up.
The thing is when I use the GetInfo method the array returned by this function contains several null values in the array, when I remove the command the array has no null strings.
Even more strange when I am operating on the array in other functions it appears that the array looses some of its properties when the GetInfo method is used. So for instance I am able to use the foreach loop to iterate through array values but I cannot access an array value by index such as $array[1].
By simply commenting out the GetInfo method call in the function the array returned seems to function normally and you can access array values by index.
I have another function that also uses GetInfo and returns a hash table, when I try to operate on the returned hashtable I cannot access values using a key value such as $hashtable['key'], but I can access them using $hashtable.key. I know this is really weird and can't really think what it could be
Has any one else experienced a similar problem.
You're missing an argument.
Function Test
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[String]
$Variable
)
Write "$Variable"
}

Powershell returns wrong result

I came across this weird issue in Powershell (not in other languages). Could anyone please explain to me why this happened?
I tried to return a specified number (number 8), but the function keeps throwing everything at me. Is that a bug or by design?
Function GetNum() {
Return 10
}
Function Main() {
$Number10 = GetNum
$number10 #1 WHY NO OUTPUT HERE ??????? I don't want to use write host
$result = 8 # I WANT THIS NUMBER ONLY
PAUSE
return $result
}
do {
$again = Main
Write-Host "RESULT IS "$again # Weird Result, I only want Number 8
} While ($again -eq 10) # As the result is wrong, it loops forever
Is that a bug or by design?
By design. In PowerShell, cmdlets can return a stream of objects, much like using yield return in C# to return an IEnumerable collection.
The return keyword is not required for output values to be returned, it simply exits (or returns from) the current scope.
From Get-Help about_Return (emphasis added):
The Return keyword exits a function, script, or script block. It can be
used to exit a scope at a specific point, to return a value, or to indicate
that the end of the scope has been reached.
Users who are familiar with languages like C or C# might want to use the
Return keyword to make the logic of leaving a scope explicit.
In Windows PowerShell, the results of each statement are returned as
output, even without a statement that contains the Return keyword.
Languages like C or C# return only the value or values that are specified
by the Return keyword.
Mathias is spot on as usual.
I want to address this comment in your code:
$number10 #1 WHY NO OUTPUT HERE ??????? I don't want to use write host
Why don't you want to use Write-Host? Is it because you may have come across this very popular post from PowerShell's creator with the provocative title Write-Host Considered Harmful?
If so, I encourage you to read what I think is a great follow-up/companion piece by tby, titled Is Write-Host Really Harmful?
With this information, it should be clear that as Mathias said, you are returning objects to the pipeline, but you should also be armed with the information needed to choose an alternative, whether it's Write-Verbose, Write-Debug, or even Write-Host.
If I were going to be opinionated about it, I would go with Write-Verbose, altering your function definition slightly in order to support it:
function Main {
[CmdletBinding()]
param()
$Number10 = GetNum
Write-Verbose -Message $number10
$result = 8 # I WANT THIS NUMBER ONLY
PAUSE
$result
}
When you invoke it by just calling $again = Main you'll see nothing on the screen, and $again will have a value of 8. However if you call it this way:
$again = Main -Verbose
then $again will still have the value of 8, but on the screen you'll see:
VERBOSE: 10
likely in differently colored text.
What that gives is not only a way to show the value, but a way for the caller to control whether they see the value or not, without changing the return value of the function.
To drive some of the points in the articles home further, consider that it's not necessarily necessary to invoke your function with -Verbose to get that.
For example, let's say you stored that whole script in a file called FeelingNum.ps1.
If, in addition to the changes I made above, you also add the following to the very top of your file:
[CmdletBinding()]
param()
Then, you still invoked your function "normally" as $again = Main, you could still get the verbose output by invoking your script with -Verbose:
powershell.exe -File FeelingNum.ps1 -Verbose
What happens there is that using the -Verbose parameter sets a variable called $VerbosePreference, and that gets inherited on each function called down the stack (unless it's overridden). You can also set $VerbosePreference manually.
So what you get by using these built-in features is a lot of flexibility, both for you as the author and for anyone who uses your code, which is a good thing even if the only person using it is you.