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]
}
Related
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 ...
}
I wrote the following function to split a string into an array of strings. There are circumstances when the input is $null, in which case the function should return $null, or when the input already is a string array, in which case the input should be returned as-is.
function Split-Tests($tests)
{
if ($tests -eq $null)
{
return $tests
}
if (($tests.GetType() -eq [string[]]) -and $tests.Count -ne 1)
{
return $tests
}
return ([string]$tests).Split(",")
}
The function should return either $null, or a string array. However, when I call this function like Split-Tests "1,2,3", the returned value has type object[] even though the string.Split function returns string[].
I tried an explicit cast to string[] (return [string[]](([string]$tests).Split(","))) instead, and I tried the [OutputType([string[]])] attribute, but the return type remained at object[].
As a final resort, I cast the result of the function invocation to [string[]]. That works, but I would rather define the return type within the function that outside it. Can you help me?
Edit: I found this answer indicating that I could add a comma between "return" and the return value. Unfortunately, in my case it didn't help. Not even "Write-Output" mentioned in this reply made a change.
Edit again: The comma trick did it, I must've done something wrong in my first attempt.
This is the normal behaviour. Unlike other programming languages, PowerShell unrolls arrays and outputs them element-by-element to the pipeline, as a stream. Even the return statement in PowerShell doesn't actually return the given object as-is, but outputs to the pipeline as well.
I. e.
return ([string]$tests).Split(",")
is just a shortcut for:
([string]$tests).Split(",") # Output to the pipeline
return # Return from the function
When the output gets captured into a variable, PowerShell just sees the individual elements as passed to the pipeline. It doesn't know about the original array type. As the values from the pipeline could be of different types, it can only create a generic object[] array, which accepts any element type.
Function Fun { return 1,2,3,'a','b','c' }
$x = Fun # Now x contains 3 ints and 3 strings.
To force output of a string[] array, you can use the unary form of the comma-operator in front of the array to prevent enumeration:
function Split-Tests( $tests)
{
if ($tests -eq $null)
{
return $tests
}
if (($tests.GetType() -eq [string[]]) -and $tests.Count -ne 1)
{
return ,$tests
}
,([string]$tests).Split(",") # No return statement needed here
}
$a = Split-Tests "1,2,3"
$a.GetType().Name # Outputs "String[]"
I want to check the count of the items in a variable and change the value of the variable depending upon the count. Further, I want to use this function to validate other variables as well. Below mentioned is the code.
$c=#()
function Status($s)
{
if($s.Count -eq "0"){
$s = "Fail"
}else{
$s="success"
}
}
Status $c
Here I expected the value of $c would be "Fail". But instead the value remains to be null.
Without fiddling around with scopes or sending variables by reference, why don't you simply have the function return 'Fail' or 'Success'?
BTW The .Count property is of type Int32, so you should not surround the value 0 with quotes, making it a string.
function Get-Status($s) {
if($s.Count -eq 0) { 'Fail' } else { 'Success' }
}
Now, if you want to overwrite the variable $c with the outcome of whatever the function returns, simply do:
$c = #()
$c = Get-Status $c # will reset variable $c to the string value of 'Fail'
P.S. I renamed the function so it conforms to the Verb-Noun naming convention in PowerShell
If you want to change multiple variables in one function you need references or scopes. Scopes will change the variable with the same name inside the function and globally. Calling a variable by reference is indifferent to the variable names outside the function.
Reference:
Working with references your variable in the function needs to be of type [ref] ( or System.Management.Automation.PSReference ). In that case the argument you use must be cast to [ref] and to this before calling the function enclose the var with brackets ([ref]$c). When using references, you can't just change the variable itself, but you need to work with its .value. The value of your reference represents your original variable. ([ref]$s.Value -eq $c)
Using that your code would look like this:
$c=#()
function Status([ref]$s) #Define $s as [ref]. only arguments of type [ref] are valid
{
if($s.value.Count -eq 0)
{
$s.Value = "Fail" #This will change the type of $c from array to string
}
else
{
$s.Value += "success" #This will recreate the array with an additional object (string)
}
}
Status ([ref]$c) #The variable must be cast to [ref] to be valid
$c
Scopes:
Normally a function is executed in a lower scope than the rest of the script. That means variables only exist in their scope and in lower scopes and changes in lower scopes won't reflect to higher scopes. However, you can directly address a variable in another scope using $<scope>:var ($script:s). The downside is you work with the variable itself. The name of the variable inside the function and outside must be the same. (reading the help for scopes is highly recommended)
Here is your code with scopes:
$s=#() #var needs to have the same name
function Status #No parameter here
{
if($script:s.Count -eq "0")
{
$script:s = "Fail" #This will change the type of $s from array to string
}
else
{
$script:s += "success" #This will recreate the array with an additional object (string)
}
}
Status
$s
For a more global function, here is a “get the value of the specified variable” function.
# PowerShell
function getVar($name){
return (Get-Variable -Name $name).Value
}
The only problem with this is that if you have two variables with different scopes, the function may return the wrong variable, therefore the wrong value.
Here is a function to set a variable. It suffers from the same cons as above though.
# PowerShell
function setVar($name, $value){
Set-Variable -Name $name -Value $value
}
You can use the -Scope $scope part to help if you need to.
Happy coding!
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.
how does putting a comma before a list affect its type?
Have a look at the following code:
function StartProgram
{
$firstList = getListMethodOne
Write-Host "firstList is of type $($firstList.gettype())"
$secondList = getListMethodTwo
Write-Host "secondList is of type $($secondList.gettype())"
}
function getListMethodOne
{
$list = new-object system.collections.generic.list[string]
$list.Add("foo") #If there is one element, $list is of type String
$list.Add("bar") #If there is more than one element, $list is of type System.Object[]
return $list
}
function getListMethodTwo
{
$list = new-object system.collections.generic.list[string]
$list.Add("foo")
$list.Add("bar")
return ,$list #This is always of type List[string]
}
StartProgram
Why is it, if you don't use a comma before returning $list in getListMethodOne it returns as type System.Object[], whereas if you do use a comma as in getListMethodTwo, it is of type List[string] as expected?
PS: I'm using PSVersion 4.0
When you return collection, than PowerShell is kind enough to unravel it for you.
Unary comma creates collection with single element, so "external" collection gets unraveled and collection you want to return is kept.
I blogged about it a while ago.
Two more things:
return is used in PowerShell to leave function early, it's not needed to return something from function (any not captured output is returned)
in PowerShell 4.0 you can use Write-Output -NoEnumerate $collection to prevent unraveling your collection.
I don't have a full answer, but I would bet this relates to PowerShell's 'flattening' behaviour.
By using the unary operator ',' you are creating a new collection wrapper around the $list object. After PowerShell 'flattens' this, you see the object that was inside the wrapper.
A fuller explaination here: http://rkeithhill.wordpress.com/2007/09/24/effective-powershell-item-8-output-cardinality-scalars-collections-and-empty-sets-oh-my/