I'm trying to figure out what dictates if a value is returned from a PowerShell function or not, and I've run into some oddities. The about_return docs say:
In PowerShell, the results of each statement are returned as output, even without a statement that contains the Return keyword.
But this seems to glaze over details. If I run this:
function My-Function {
1
[System.Console]::WriteLine("Hello")
$null
$true
$false
0
2
}
Running this returns an array of (along with printing "Hello"):
1
True
False
0
2
Which means that $null
isn't auto-returned. Then I tried incrementing since I'm doing using that in a function:
function My-Function {
$n = 1
$n
$n++
($n++)
-join @(1, 2, 3)
(-join @(1, 2, 3))
}
Returns:
1
2
123
123
So, $n
and $n++
were returned, but ($n++)
wasn't? But then when compared to another operator (-join
), it's the same in each case. Why does wrapping parenthesis around $n++
prevent it from being returned, and why don't other operators behave the same? This is even more confusing since the =
operator appears to work in the opposite way:
function My-Function {
($n = 1)
$n
$n++
($n++)
}
Returns:
1
1
2
Now wrapping the assignment causes it to be returned, whereas wrapping $n++
causes it to not be returned.
In summary, I'd just like to know a straightforward way of being able to look at a line of code in a function, and determine if it will cause a value to be returned or not.
This section discusses the specific statements from your sample functions.
See the next section for background information.
$n = 1
and $n++
are assignments and therefore do not produce output.$n
is an expression whose value is output$null
- ditto, but even though it is output, it doesn't display by default($n++)
- due to enclosure in (...)
- turns the assignment into an expression and therefore does output the assigned value (too).
(++$n)
[System.Console]::WriteLine("Hello")
prints directly to the console, which bypasses PowerShell's system of output streams.
Tip of the hat to iRon for his help.
PowerShell, following the model of traditional shells, is organized around streams - see the conceptual about_Redirection help topic for an overview of all 6 streams that PowerShell supports.[1]
That is, any statement - and therefore potentially multiple ones - in a script or function can write to any of the output streams.
The primary output stream, meant to convey data, is the success output stream (whose number is 1
), and only it is sent through the pipeline by default, and therefore by default only it is captured in a variable, suppressed, or redirected to a file.
There are two ways to write to the success output stream, i.e. to produce data output:
Explicitly, with a Write-Output
call (echo
is its built-in alias) - although that is rarely needed.
Write-Output
is needed / useful.Typically implicitly, by neither capturing, suppressing, nor redirecting output produced by an expression, command, or language statement.
In other words: Output from any command (e.g., Get-ChildItem *.txt
) or expression (e.g, 1 + 2
or (42).ToString('x')
) or even language statement as a whole (e.g, foreach ($i in 0..2) { 1 + $i }
) is sent to the success output stream by default.
Unlike in traditional programming languages, return
is not needed to produce output - in fact, its primary purpose is to exit the enclosing scope independently of any output the scope produces, though as a syntactic convenience you can combine the two aspects:
return <command-or-expression>
is in effect the same as the following two statements, the first of which (potentially) produces output, the second of which exits the scope: <command-or-expression>; return
This implicit output behavior is convenient and and allows for concise, expressive code, but can also be a pitfall: it is easy to accidentally produce output - typically from a .NET method whose return value isn't needed (see this question for an example).
iRon's GitHub feature request #15781 discusses one potential way to remedy this problem: introduction of an opt-in strict mode that only permits using explicit output statements (Write-Output
, return
) in order to produce output.
This answer shows troubleshooting techniques you can use with the currently available features.
As for assignments - e.g. $n = 1; $n += 1; ++$n; $n--
:
$a = $b = 1
, which assigns 1
to both variables: statement-internally the assignment value is passed through, but the statement as a whole has no output.(...)
, the grouping operator; e.g. ($n = 1)
both assigns 1
to variable $n
and outputs 1
, which allows it to participate in larger expressions, such as ($n = 1) -gt 0
$(...)
(subexpression operator) and @(...)
(array-subexpression operator) do not have that effect - they wrap one or more entire statement(s), without affecting the enclosed statements' intrinsic output behavior; e.g. $($n = 1)
does not produce output, because $n = 1
by itself doesn't produce output; however, $(($n = 1))
does, because ($n = 1)
by itself does.As for output enumeration behavior:
By default, PowerShell enumerates collections that are being output, in the spirit of streaming output: That is, it sends a collection's elements to the pipeline, one by one.
In the rare event that you do need to output a collection as a whole - which in general should be avoided, so as not to confound other commands participating in a pipeline, which usually do expect object-by-object input - you have two options:
, $collection
(sic; uses an aux. one-element wrapper array)Write-Output -NoEnumerate $collection
As for outputting $null
:
$null
is output to the pipeline, but by default doesn't show.
$null
by itself produces no visible output,
but the following returns $true
, demonstrating that the value was sent:
$null | ForEach-Object { $null -eq $_ } # -> $true
Note that PowerShell also has an "array-valued $null
" value that it uses to represent the lack of output from a command, which is technically represented as the [System.Management.Automation.Internal.AutomationNull]::Value
singleton. In expression contexts, this values is treated the same as $null
, but in the pipeline it behaves like an enumerable without elements and therefore sends nothing through the pipeline - see this answer for more information.
As for suppressing (discarding) unwanted output / redirecting to a file:
The best general-purpose way to suppress a statement's success output is to assign to $null
($null = ...
); e.g.:
# .Add() outputs a value, which is usually unwanted.
$null = ($list = [System.Collections.ArrayList]::new()).Add('hi')
Note: The following discusses output suppression, via $null
as the redirection target, but applies analogously to redirecting output to a file, by specifying a file name or path as the target.[2]
To selectively suppress a different output stream, prefix >$null
with its number; e.g. 3>$null
suppresses warning stream output.
To suppress output from all streams, which in the case of external programs covers both stdout and stderr, use redirection *>$null
.
As for merging output streams:
1
) can be merged into.2>&1
and/or 3>&1
), or merge all (others): *>&1
2
) are [System.Management.Automation.ErrorRecord]
instances - see this answer for more information.As for bypassing PowerShell's system of streams:
Out-Host
and [Console]::WriteLine()
calls bypass PowerShell's output streams and write directly to the host / console (terminal). (A host is any environment that hosts the PowerShell engine, which is typically, but not necessarily a console (terminal); examples of other hosts are the PowerShell SDK and the host used in PowerShell remoting).
Write-Host
formerly unconditionally bypassed PowerShell's output streams and still goes to the host by default, but - since PowerShell version 5 - routes its output via the information stream (stream number 6
), where it can be captured / redirected on demand - see this answer for more information.
As for how output is formatted:
If output isn't captured, suppressed, or redirected, it is sent to the host (console) by default, where it is rendered based on PowerShell's rich and customizable for-display output-formatting system. See this answer for a concise overview.
Note that the resulting representations are designed for the human observer, not for programmatic processing. While PowerShell maintains a clear separation between the actual data and its representation, the caveat is that you do end up with just the for-display, string representation in the following scenarios:
Out-File
or its effective aliases, the redirection operators >
and >>
Export-Csv
or ConvertTo-Csv
) or JSON (using ConvertTo-Json
).As for how the outside world sees PowerShell's output streams:
IPC (inter-process communication) at the OS level knows only two output streams: stdout (standard output) and stderr (standard error), which forces PowerShell to map its 6 output streams onto these two in order to return streaming output to an outside caller.
While it would make sense to map PowerShell's success output stream to stdout and all other streams to stderr, unfortunately all streams are reported via stdout by default as of PowerShell 7.2 - although selectively redirecting stderr in the calling process (typically with 2>
) does send PowerShell's error stream (only) to that redirection target. See the bottom section of this answer for more information.
Also note that PowerShell as of version 7.2 only ever communicates via text (strings) with outside callers as well as with external programs called from inside a PowerShell sessions, which means that character-encoding issues can arise - see this answer for more information.
[1] Note that PowerShell has no concept of an input stream as such, and therefore also does not support the stdin redirection operator <
familiar from other shells. Instead, commands receive streaming input (only) via the pipeline. In order to receive data from the outside world, via the PowerShell CLI's stdin stream, the automatic $input
variable must be used - see this answer.
[2] Using >
(or >>
) to redirect to a file effectively uses the Out-File
cmdlet behind the scenes, and therefore its default character encoding, which is "Unicode" (UTF-16LE) in Windows PowerShell, and BOM-less UTF-8 in PowerShell (Core) 7+. However, in PowerShell version 5.1 and above you can control this encoding via the $PSDefaultParameterValues
preference variable - see this answer.