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!' }
}
Related
What can I do so that I write the desired phrase in brackets and it will be accepted?
Where exactly to get the "basis" in order to make a return using the "PrintString" method?
# Example 1
$Object.SetString("gamarjoba")
# Example 2
$Object.PrintString()
# Returns
GAMARJOBA
Here is one of my attempts:
class Object {
[string]$gamarjoba
[string]SetString() {
return Write-Host "gamarjoba"
}
[string]PrintString() {
return Write-Host "gamarjoba".ToUpper()
}
}
I understand that this is a very basic question, but I have already spent too much time on it.
You're probably looking for this:
class MyObject {
[string] $gamarjoba = '' # instance variable
# Method has no return type; accepts a string parameter
# and assigns it to instance variable $this.gamarjoba
[void] SetString([string] $gamarjoba) {
$this.gamarjoba = $gamarjoba
}
# Returns the uppercased form of the $this.gamarjoba instance variable.
# Do NOT use Write-Host.
[string] PrintString() {
return $this.gamarjoba.ToUpper()
}
}
$obj = [MyObject]::new() # create an instance
$obj.SetString('aha') # set the string
$obj.PrintString() # print the uppercased string
Note that I've named the class MyObject, because Object would clash with the root class of the .NET type hierarchy.
As for what you tried:
return Write-Host "gamarjoba".ToUpper()
Fundamentally, do NOT use Write-Host to return or output data - it is meant for printing information to the display (console), and bypasses the success output stream and thereby the ability to send output to other commands, capture it in a variable, or redirect it to a file - see this answer for more information.
In the context of class definitions, use only return to return data from a method, passing it an expression or command as-is (e.g, return 'foo'.ToUpper() or return Get-Date -Format yyy)
PowerShell classes participate in the system of output streams only in a very limited way:
returning a value from a method writes it to the success output stream.
Notably, implicit output and use of Write-Output (i.e. what you would use in a script or function) are not supported and quietly ignored. (While you can use return Write-Output ..., there's no good reason to do so.)
throwing an error writes it to the error stream - but you'll only see that if you catch or silence such a fatal-by-default error.
Notably, using Write-Error to write non-terminating errors does not work and is quietly ignored.
However, you can write to all other output streams using their respective Write-* cmdlets, such as Write-Warning.
Lets say I have a function like this:
Function Hello(){
Write-Host "Hello, World"}
when used on its own it works perfect but I would also like for it to receive pipeline input:
$MyString = "Something Else please"
$MyString | Hello
In the second example, Something Else please should be printed instead of the default value of Hello, World.
I have searched and searched and have turned up empty handed. everything out there keeps mentioning default parameters. I am not looking to use parameters.
Someone please point me in the right direction.
Declare a parameter, and:
assign it a default value.
make it pipeline-binding, and be sure to process each pipeline input object in the function body, namely via a process block
Note: Declaring a parameter pipeline-binding (with [Parameter(ValueFromPipeline)] below) implicitly makes a function an advanced one, which has (generally beneficial) behavioral implications - see this answer.
function Hello {
param(
[Parameter(ValueFromPipeline)]
$InputObject = 'Hello, World' # default value
)
process {
# Called for each pipeline input object, or once with the default value.
Write-Host $InputObject
}
}
For robustness, it's generally preferable to declare parameters explicitly, as shown above.
The - less desirable - parameter-less, simple-function alternative is the following, which collects all pipeline input up front, as it too implicitly runs in an end block, and uses the automatic $input variable and $MyInvocation.ExpectingInput to detect if pipeline input was provided, as suggested by Santiago Squarzon:
function Hello {
if ($MyInvocation.ExpectingInput) { # input from the pipeline
$input | Write-Host # pass each pipeline input object to Write-Host
} else { # no pipeline input -> use default value
'Hello, World' | Write-Host
}
}
As for what you tried in your answer:
By not using a process block, in effect only the last input object from the pipeline is bound to parameter $InputObject, because a function body without (any one of) begin, process and end blocks implicitly runs in an end block, i.e. after all pipeline input has been received.
Generally, there's no good reason to type a parameter [PSObject] or [PSObject[]], given that [psobject] is a usually invisible helper type used behind the scenes.
Not typing a parameter is the same as typing it [object], which is what should be used to accept arguments of any type.
Typing it [array] is the same as typing it [object[]], but note that if you type a pipeline-binding parameter as an array, each individual input object is automatically converted to an array - which slows down processing.
Only if you need to accept multiple values as a single command-line argument (as opposed to via the pipeline) is declaring a parameter as an array warranted - and when you do, you then need to enumerate each bound parameter value in your process block, as it may itself be a true array.
As an aside: That pipeline-binding parameters declared with scalar types only accept multiple values via the pipeline, but not also implicitly as an argument is the subject of GitHub issue #4242
By using this syntax for you function you can create what I think you're after.
Clear-Host
Function Hello {
Param (
[Parameter(Mandatory=$False,ValueFromPipeline=$True)]
[String] $MyText = "Hello"
)
Write-Host "$MyText, World"
}
Hello
"It's A Wonderful" | Hello
Output:
Hello, World
It's A Wonderful, World
PS>
Here is what I arrived at after #Santiago Squarzon pointed me to the right direction
Function Hello {
Param (
[Parameter(Mandatory=$False,ValueFromPipeline=$True)]
[PSObject[]] $InputObject,
)
if ($PSCmdlet.MyInvocation.ExpectingInput) {
"Data received from pipeline input: '$($InputObject)'"
}
else {
Write-host "Hello World"
}
}
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.
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.
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.