How to define the return type / OutputType of a function - powershell

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.

Related

Default condition of Switch parameter never gets triggered

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!' }
}

Why does my PowerShell function return Object[] instead of string[]?

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[]"

PowerShell Where-Object vs. Where method

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.

Strong type checking for non existent file declared in a function parameter

I am writing a PowerShell function that carries out some operation on a file, the path to the file is passed to the function as a parameter. I'm a fan of strong typing and parameter validation so instead of just passing the file path as a System.String I've defined the parameter like so:
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.PathInfo]$PathInfo
Normally I would use Resolve-Path in the calling code to get an object of type System.Management.Automation.PathInfo that I could pass to this parameter however in this case it is legitimate for the file to not yet exist and hence Resolve-Path would throw an error.
Is it possible to instantiate an instance of System.Management.Automation.PathInfo for a none-existent file? If so, how? If not, do you have a suggestion for how I might pass a non-existent file path to a function and still have strong type checking.
Although using the [System.IO.FileInfo] type would probably be the neatest solution in this case (doing something to a file), you might run into problems if you're given a path to a folder, since .Exists returns False in such cases. You'd want to use [System.IO.DirectoryInfo] instead...
Thinking a bit more generally you could use a validation script, esp. one that calls some sort of testing function, for example the following should allow parameters that are either $null or a valid [System.Management.Automation.PathInfo] type.
function Test-Parameter {
param($PathInfo)
if([System.String]::IsNullOrEmpty($PathInfo)) {
return $true
} elseif($PathInfo -is [System.Management.Automation.PathInfo]) {
return $true
} else {
return $false
}
}
And then you use this a [ValidateScript({...})] check your parameter meets those (arbitrary) conditions:
function Do-Something {
param(
[Parameter()]
[ValidateScript({Test-Parameter $_})]
$PathInfo
)
....
}

PowerShell AST FindAll method take a scriptblock with $args[0]

I have been working with the PowerShell AST to create some custom rules for PSScriptAnalyzer.
In a lot of the example code for AST, there is one line that I don't understand. Here is an example.
First parse a file, in this case, the current open file in the ISE.
$AbstractSyntaxTree = [System.Management.Automation.Language.Parser]::
ParseInput($psISE.CurrentFile.Editor.Text, [ref]$null, [ref]$null)
This makes sense so far. Let's say that we want to look for all the ParameterAst objects. The code that I have seen to do this is below.
$params = $AbstractSyntaxTree.FindAll({$args[0] -is [System.Management.Automation.Language.ParameterAst]}, $true)
This line of code is calling FindAll and passing in a scriptblock, that seems to be acting as a filter, so that only ParameterAst objects are returned.
What I don't understand here is how $args[0] fits into this call. How are any parameters actually getting passed into the scriptblock when the FindAll method is invoked?
FindAll method has following signature (from msdn):
public IEnumerable<Ast> FindAll (
Func<Ast,bool> predicate,
bool searchNestedScriptBlocks
)
So first argument is a delegate that takes Ast as input, and returns bool.
In Powershell you can create such delegate like that:
$delegate = { param($ast) $ast -is [System.Management.Automation.Language.ParameterAst] }
Or without declaring parameter:
$delegate = { $args[0] -is [System.Management.Automation.Language.ParameterAst] }
FindAll method will then do something like that (pseudocode):
foreach ($node in $allNodes) {
$shouldAdd = & $delegate $node <-- this is how $node gets passed to your delegate
if ($shouldAdd) {
<add the node to the output list>
}
}
Think of the scriptblock as an anonymous callback function.
It's really the same thing that happens when you use Where-Object { $someCondition }.
.FindAll finds all the (things) and for each one it calls the function you provided it. It's apparently expecting a [bool] result, and returning the objects that satisfied the conditions present in the callback.
In a function or script or scriptblock in powershell, you can have named parameters that are explicitly defined, or you can reference parameters without declaring them using the $args array, which is what's happening here.
Using a scriptblock as a callback is similar to using it for an event:
$Args
Contains an array of the undeclared parameters and/or parameter
values that are passed to a function, script, or script block.
When you create a function, you can declare the parameters by using the
param keyword or by adding a comma-separated list of parameters in
parentheses after the function name.
In an event action, the $Args variable contains objects that represent
the event arguments of the event that is being processed.