I am trying to pass an exception type to be ignored to a function, but also provide a default exception type to ignore. And that default is not working. So, given
function Test {
param (
[Type]$ExceptionType = [System.Management.Automation.ItemNotFoundException]
)
if ($ExceptionType -is [System.Management.Automation.ItemNotFoundException]) {
Write-Host "Yes"
} else {
Write-Host "No: $ExceptionType"
}
}
I would expect that running
Test
would return Yes because of the default value. But running
Test -ExceptionType:([System.Management.Automation.PSArgumentOutOfRangeException])
should return No: System.Management.Automation.PSArgumentOutOfRangeException, which is does.
The problem is somehow in the conditional, because
if ([System.Management.Automation.ItemNotFoundException] -is [System.Management.Automation.ItemNotFoundException]) {}
also returns false. But elsewhere, where $PSItem.Exception is the exception I am evaluating the ignore on,
if ($PSItem.Exception -is $waitOnExceptionType) {}
seems to work fine. So, what am I doing wrong, and why is the conditional not working in this case? I have also tried wrapping the type in ( ) as you have to do with the argument, as well as using a second variable for the conditional, like so
$defaultException = ([System.Management.Automation.ItemNotFoundException])
if ($ExceptionType -is $defaultException) {
But to no avail.
Hmm, I suspect I have bigger problems. Per #mathias-r-jessen I have revised to
if ($ExceptionType -eq ([System.Management.Automation.ItemNotFoundException])) {
Write-Host "Yes"
} else {
Write-Host "No: $($ExceptionType.GetType().FullName)"
}
And now the conditional works with the default, but when passing
Test -ExceptionType:([System.Management.Automation.PSArgumentOutOfRangeException])
I return No: System.RuntimeType rather than the correct type. So anything I do later that depends on the actual type is problematic.
The -is operator is for testing whether a given value is an instance of a type.
To test whether a given type is a subclass of another, use the Type.IsSubclassOf() method:
$ignoredBaseType = [System.Management.Automation.ItemNotFoundException]
if($ExceptionType -eq $ignoredBaseType -or $ExceptionType.IsSubClassOf($ignoredBaseType)){
# ignore ...
}
Related
What is the right way to check value of a switch?
function testSwitch
{
Param(
[switch] $swth
)
Write-Host "Value of swth is $swth"
if($swth.IsPresent){
Write-host "Switch is present"
}
if($swth){
Write-Host "Switch is present"
}
}
testSwitch -swth
I know both if statement works fine, but how?
$swth.IsPresent and $swth can be used interchangeably, because in a Boolean context (such as an if conditional) $swth (an instance of type [switch] representing a switch parameter) effectively returns the value of the .IsPresent property.[1]
In fact, $swth is probably preferable[2], not just for concision, but because the .IsPresent property is somewhat confusingly named:
.IsPresent doesn't indicate the presence of the switch per se, but whether its value is $true.
While specifying a switch by itself - -swth - implies both, the same isn't true if you explicitly set it to $false: Passing -swth:$false makes .IsPresent return $false, even though the switch is clearly present.
Passing $false explicitly isn't common, but has its uses, such as when opting out of a confirmation prompt with -Confirm:$false, and when programmatically constructing arguments.
Therefore, if you want to distinguish between the user not passing the switch and the user passing it with value $false, .IsPresent won't help you - you'll have to use $PSBoundParameters.ContainsKey('swth')
[1] On conversion to Boolean, which happens in method LanguagePrimitives.IsTrue(), PowerShell calls a [switch] instance's .ToBool() method, which in turn returns the private backing variable behind the .IsPresent property.
[2] The only caveat is that you must be aware that $swth is not a Boolean ([bool]), but of type [switch], which may matter in other contexts.
It would be more simple to just use an if/else clause:
Function Get-FruitDetails {
[CmdLetBinding()]
Param (
[String]$Fruit,
[Switch]$Yellow
)
Write-Verbose "Get fruit details of $Fruit"
$Result = [PSCustomObject]#{
Fruit = $Fruit
Yellow = $null
}
if ($Yellow) {
Write-Verbose 'Color is yellow'
$Result.Yellow = $true
}
else {
Write-Verbose 'Color is not yellow'
$Result.Yellow = $false
}
$Result
}
Get-FruitDetails -Fruit 'Banana' -Yellow -Verbose
Get-FruitDetails -Fruit 'Kiwi' -Verbose
This will output the following:
Fruit Yellow
----- ------
Banana True
Kiwi False
Some tips:
Avoid using Write-Host, it's meant for console programs. You're better of using Write-Verbose and adding the -Verbose switch if you want to see the messages
Try to use PowerShell approved verbs for function names. These can be easily found using Get-Verb. It will make it easier for others to see what your function does Get, Set, Remove, ...
I've tried this code in PowerShell ISE and VS Code with the same strange result. Without a breakpoint, the output is EMPTY, but with a breakpoint in the line with "NULL", the output is NULL (as expected). Why?
function demo {
param(
[string] $value = [NullString]::Value
)
if ($null -eq $value) {
"NULL"
} elseif ($value -eq '') {
"EMPTY"
} else {
"$value"
}
}
demo
I know now, that PowerShell will always convert a non-string value (e.g. $null or [NullString]::Value) to an (empty) string, when you use the type modifier [string] for a parameter.
Fine, I can live with that, but it's hard to figure that out by yourself, if debugging is so weird in this case.
PetSerAl, as many times before, has provided the crucial pointer in a comment on the question:
A known optimization bug is the most likely cause (as of Windows PowerShell v5.1 / PowerShell Core v6.1.0), and that bug just happens to be masked when the code is run in the Visual Studio Code or ISE debugger.
You can therefore use the same workaround mentioned in the linked bug report: place a call to Remove-Variable anywhere in the function body (its presence is enough - the call needn't actually be made at runtime):
function demo {
param(
[string] $value = [NullString]::Value
)
# Workaround for optimization bug
if ($False) { Remove-Variable }
if ($null -eq $value) {
"NULL"
} elseif ($value -eq '') {
"EMPTY"
} else {
"$value"
}
}
demo
Now you consistently get "NULL" as the output, whether debugging or not.
However, it's best to restrict use of [NullString]::Value to what it was designed for: passing null to string-typed parameters of .NET methods - see below.
As for why use of [NullString]::Value is needed at all in order to pass $null to a string parameter / store $null in a [string] variable, given that .NET strings can normally store null ($null) directly:
By (historical) design, PowerShell converts $null to '' (the empty string) when you assign it to a [string] variable; here's the rationale:
From https://github.com/PowerShell/PowerShell/issues/4616#issuecomment-323530442:
The thinking behind the design was that in most ways, $null and the empty string both represent the same error condition and that in the rare case where a distinction was important, $PSBoundParameters would be sufficient to distinguish between knowing a value was provided or not.
Given that even passing $null directly performs conversion to '' when passing arguments to string-typed .NET methods, you couldn't pass null to such methods up to v2.
To remedy that, version 3 introduced [NullString]::Value, which explicitly signals the desire to pass $null in a string context.
(The alternative - making PowerShell strings default to $null and allowing direct assignment of $null - was considered a change that would break too many existing scripts.)
Using [NullString]::Value beyond its intended purpose - i.e., for passing null to string parameters in .NET methods - is problematic, given that PowerShell doesn't expect [string] variables to contain $null in other contexts.
Fixing the above-mentioned optimization bug would help in the scenario in the question, but there may be other pitfalls.
Difference between $null and ""
Empty string is not the same as null; you'd need to test specifically
for that. Null is the non-existence of an object, Whereas a string is
an object even if it's empty.
That's a common gotcha in PowerShell. For whatever reason, it won't
let you assign a $null value to a string variable (or a string
parameter to a .NET type); it gets converted to an empty string. This
can cause some headaches when calling .NET methods that treat null and
empty strings differently, which is why they later added (in v3, if I
remember correctly), the [System.Management.Automation.NullString]
class. If you want to call such a method, you do this:
[SomeClass]::SomeMethod([nullstring]::Value)
[string]$camp = $null
Will assign $Null to $Camp, but since the [string]-part forces $camp
to be of the type string, $Camp will be assigned the value of
[String]$Null
[String]$Null will force a conversion of $Null (which is basically a
non-existing object) to a string and in PowerShell that results in an
empty string.
As far as PowerShell is concerned, that's basically correct. However,
in the .NET Framework, strings really can be null, and there's a
difference between a null string reference and an empty string.
Sometimes that causes confusion when you're looking at .NET
documentation and wondering why it doesn't seem to work properly from
PowerShell.
Use [string]::IsNullOrEmpty($variable)
https://powershell.org/forums/topic/difference-between-null-and/
See also...
How can I check if a string is null or empty in PowerShell?
[string]::IsNullOrEmpty(...)
How can I check if a string is null or empty in PowerShell?
* Update *
Changing your code to this...
function demo {
param(
[string]$value
)
if ([string]::IsNullOrEmpty($value))
{
"NULL"
} elseif ($value -eq '') {
"EMPTY"
} else {
"$value"
}
}
With or without debug (breakpoint set on 'NULL' and 'EMPTY') effort, the results returned are the same on my system ISE/VSCode
# Results
demo
NULL
demo -value ''
NULL
demo -value ' '
demo -value test
test
* Modification to show the elseif handling whitespace *
With or with the conditions previously stated
function demo {
param(
[string]$value
)
if ([string]::IsNullOrEmpty($value))
{
"NULL"
} elseif ([string]::IsNullOrWhiteSpace($value)) {
"EMPTY or a White Space"
} else {
"$value"
}
}
# Results
demo
NULL
demo -value ''
NULL
demo -value ' '
EMPTY or a White Space
demo -value test
test
Returning an empty HashSet from a function turns the object to null. What causes this behaviour and is there any way to work around it? I don't want to have to special case the empty set case everywhere (i.e. instead of a nice clean if ($set.Contains(something)) now it has to be if ($set -and $set.Contains(something))).
function GetASet() {
$someSet = New-Object System.Collections.Generic.HashSet[int]
$someSet
}
[System.Collections.Generic.HashSet[int]]$set = GetASet
$set -eq $null # this is true
Powershell unrolls collections by default (although not very consistently). You need to hint it to explicitly return collection in your function:
#($someSet)
,$someSet
Write-Output -NoEnumerate $someSet
Try this for your function
function GetASet() {
New-Object System.Collections.Generic.HashSet[int]
}
An interesting and weird thing I noticed writing PowerShell classes lines:
class A {
[object] WhereObject(){
return #(1,2) | Where-Object {$_ -gt 2}
}
[object] Where(){
return #(1,2).Where( {$_ -gt 2})
}
}
$a = new-object A
$a.WhereObject() # Throw exception Index was out of range. Must be non-negative and less than the size of the collection.
$a.Where() # Works well
It looks like it is by design. Why does it work so?
Workaround
Function which explicitly convert "empty" value to $null:
function Get-NullIfEmpty {
param(
[Parameter(ValueFromPipeline=$true)][array] $CollectionOrEmtpy
)
begin { $output = $null }
process
{
if($output -eq $null -and $CollectionOrEmtpy -ne $null){
$output = #()
}
foreach ($element in $CollectionOrEmtpy)
{
$output += $element
}
}
end { return $output }
}
In this case, the method will look like:
[object] WhereObject() {
return #(1,2) | Where-Object {$_ -gt 2} | Get-NullIfEmpty
}
I tried to return an empty array from the class method, but it is also tricky because for a regular function an empty array means "nothing" as well. If you have a call chain like method1 -> function -> method2 - method1 throw the same exception. Because the function converts an empty array to nothing.
So converting to $null is optimal in my case :)
The (PowerShell v4+) .Where() method, which is evaluated in expression mode, always returns an instance of [System.Collections.ObjectModel.Collection[psobject]]:
If no input objects match, that instance is simply empty (it has no elements and its .Count property returns 0).
By contrast, the Where-Object cmdlet uses pipeline semantics, which implies the following output behavior:
If nothing is output (if nothing matches the filter script block), the return value is a "null collection", which is technically the [System.Management.Automation.Internal.AutomationNull]::Value singleton.
If a single item matches, that item is output as-is.
If multiple items match and they are collected in a variable / evaluated as part of an expression, they are collected in an [object[]] array.
As for the specific symptom - which Bruce Payette's answer has since confirmed to be a bug.
Update: The bug is fixed since at least v7; returning "nothing" (AutomationNull) is now coerced to $null; see the original bug report on GitHub.
An internal [List[object]] instance is used to collect the method call's output, executed via an internal pipeline. If that internal pipeline outputs "nothing" - i.e., [System.Management.Automation.Internal.AutomationNull]::Value - no object is added to the list. However, subsequent code assumes that there is at least one object in the list and blindly accesses index 0, causing the error at hand.
A simpler reproduction of the problem:
class A {
# Try to return [System.Management.Automation.Internal.AutomationNull]::Value
# (which is what `& {}` produces).
[object] WhereObject(){ return & {} }
}
$a = new-object A
$a.WhereObject() # Throw exception Index was out of range. Must be non-negative and less than the size of the collection.
As for the desirable behavior:
It seems that the fix will result in $null getting output if the method's code returns the "null collection", using C#'s default-value feature - see this comment.
The .Where() operator always returns a Collection<PSObject>. The pipeline case however, returns nothing. This is a problem because the code that invokes the scriptblock expects there to be an object in the result List i.e. result.Count == 1. There are no objects in the pipeline case so you get an index-out-of-range error. So this is a bug. We should still generate an error but it should be "non-void methods must return a value" or some such. BTW - the code in question is here.
Because in PowerShell there is no
[ValidateNotNullOrWhiteSpace()]
Parameter attribute, is it better to use
[ValidateNotNullOrEmpty()]
as the parameter attribute, then look for whitespace inside the function, or should i use
[ValidateScript({ -not ([String]::IsNullOrWhiteSpace($_)) })]
as the parameter attribute.
im not sure what the NotNullOrEmpty attribute is really good for, because 99% of time, i don't want this to ever work:
My-Cmdlet -ParameterName " "
but because " " is still a string, it will pass the NotNullOrEmpty attribute.
The [ValidateNotNullOrEmpty()] is good for doing exactly what it says it will.
To answer your first question, I would use the [ValidateScript(...)] method you outlined.
One thing to keep in mind is that the error message for [ValidateScript()] is usually terrible and doesn't help the end user. As a workaround, you can do this:
[ValidateScript( {
![String]::IsNullOrWhiteSpace($_) -or throw 'Your string is null or contains whitespace'
} )]
You can also include an exception class in the throw like:
throw [System.ArgumentException]'Blah blah whitespace'
Why the -or throw syntax works
Boolean operators in PowerShell work like many other languages in that they do "shortcutting". They stop evaluating the expression as soon as the outcome is definite. So for $false -and $(sleep -min 10) it doesn't take 10 minutes, because the second expr is never evaluated.
Similarly, $true -or $(something) will not evaluate the second, because it will always be true if the first expression is true.
So in this case it's similar. ValidateScript needs a bool. If the condition we care about is true, it will stop there. If it's not, the second expression is evaluated, and since that's a throw it will just fail, but in this case you can control the failure message (and exception class if you want).
An easy workaround would be to just Trim() the whitespace and call [string]::IsNullOrEmpty() inside [ValidateScript()]:
[ValidateScript({
if ([string]::IsNullOrEmpty($_.Trim()))
{
throw "Your string is null or empty."
} else {
$true
}
})]
Which will handle all $null, "" and " " cases, since all whitespace " " will just be converted to "".