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.
Related
I have a switch statement inside a function like so:
function SomeFunc {
[CmdletBinding()]
Param(
[Parameter(Position = 0)]
[switch]$History
)
Process {
switch ($PSBoundParameters.keys) {
'History' {$PSBoundParameters.keys}
Default {write-host "No Parameters used"}
}
}
}
If I invoke SomeFunc -History "History" gets printed, as expected.
but for the life of me, I cannot get the default condition to trigger, I am expecting "No Parameters used" to print when I invoke only "SomeFunc"
Any help on this would be really wellcome.
tl;dr
Because $PSBoundParameters.Keys is an empty collection when no arguments are passed, the switch statement's body is never entered.
Use $PSBoundParameters.Count -eq 0 to detect if no parameters were passed.
It isn't obvious, but the switch statement
enumerates its input, just like the pipeline does.
That is, if you provide an enumerable object to switch, the enumerated elements are processed, one by one.
Typical examples of enumerables are arrays or collections, though not hashtables. See the bottom section of this answer for details on what PowerShell considers enumerable.
It follows that if there is nothing to enumerate, no processing takes place at all - not even the default branch is entered.
In short: Any empty enumerable causes the switch statement's body to be skipped, notably with:
an empty array or collection (one without elements), such as the empty [System.Collections.Generic.Dictionary`2+KeyCollection[string, object]] instance that the automatic $PSBoundParameters variable's .Key property returns when no parameters are bound.
a call to a command that produces no output
The reason is that PowerShell represents the no-output case with a special object sometimes called "AutomationNull", which can be thought of as an "enumerable $null"; that is, in an enumeration context it behaves like an empty collection rather than $null - see this post for details.
Two simple examples in which a switch statement's body is skipped:
# An empty array has nothing to enumerate -> body is not entered.
switch (#()) {
default { 'Never get here!' }
}
# Ditto for a command that produces no output.
switch ((Get-ChildItem -Filter *NoSuchFiles*)) {
default { 'Never get here!' }
}
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[]"
Why is the following changing type?
function SomeFunction($SomeParameter){
return $SomeParameter
}
I guess I need to set a return type, but how?
An example is using:
$NewFolder=Join-Path $CurrentFolder -ChildPath $FolderName
$Tmp=SomeFunction($NewFolder)
Now $Tmp is an array and not just a path
While this answer explains the behavior you're seeing, here I will attempt to answer the actual question: how to declare the expected output type of a function!
You do so by adding an [OutputType] attribute to the param() block of your function - so the first thing you'll want to do is to skip the C#-style param list and declare a proper param block instead:
function SomeFunction
{
param($SomeParameter)
return $SomeParameter
}
Now we just need to add the [OutputType] attribute decorator:
function SomeFunction
{
[OutputType([string])]
param($SomeParameter)
return $SomeParameter
}
since we're just returning the parameter argument value as-is in this example, we should play nice and make sure it's actually also a string:
function SomeFunction
{
[OutputType([string])]
param(
[string]$SomeParameter
)
return $SomeParameter
}
Worth noting that [OutputType()] makes no guarantees as to the type of objects emitted during execution, it's simply a way for the author of a function to indicate the intended output type.
Read more about [OutputType] in the about_Functions_OutputTypeAttribute help file
Your issue is per "design". PowerShell will return an array in chunks so that it can be forwarded the PowerShell pipeline.
Example:
SomeFunction -SomeParameter #(1,2,3,4) | Where-Object { $_ -gt 2 }
Without this behavior pipelining the output of the function to another function/cmdlet won't be possible.
If you want to return an array you can change to code to:
function SomeFunction($SomeParameter){
<#
# Through the unary operator we can return an array with one entry.
# This entry contains the original array.
#>
,$SomeParameter
}
Another option would be the use of #() when at the calling side:
function SomeFunction($SomeParameter){
# return to pipelin
$SomeParameter
}
$array = #(SomeFunction -SomeParameter 1,2,3,4)
There is also this reddit answer explaining the behavior in more detail.
Hope that helps.
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]
}
Anyone have any ideas why the following code would produce an error, see additional comments after the function for more details
function callee ([Hashtable]$arg0) {
[Hashtable]$hashtable = #{}
$hashtable = $arg0
$hashtable.add('passed', $True)
# $hashtable ######## toggle this line
$type = $hashtable.GetType()
Write-Host "$type"
return $hashtable
}
function caller {
[Hashtable]$hashtable = #{'00'='0'}
$hashtable = callee $hashtable ##### returns error here
$hashtable.add('returned', $True)
$hashtable
}
caller
error message:
Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Collections.Hashtable".
I receive the error on a variety of instances, I tried to narrow it down to an example that is easy to reproduce. It looks like it is changing the hashtable to an object array and that is why it won't return it? It allows me to modify the hashtable and return it but when I try to display it it changes it? This is the same effect I get when I start adding code to the callee function?
When you uncomment # $hashtable you're outputting two things from the function. The result of the function is everything 'output' from it, and PowerShell will automatically wrap multiple outputs into an array. The return statement is a short-circuit convenience and should not be confused with the only way to return a value from the function.