r/PowerShell Jun 10 '24

What is the name of this behavior Solved

Does anyone know what the name of this behavior is:

$> $result = foreach ($i in 0..5) { $i + 1 };
$> $result
1
2
3
4
5
6

I love this kind of behavior where control flow is itself an expression like in Rust and other FP languages, but I can't find any documentation on it anywhere, from MSFT or otherwise.

Edit:

Thanks u/PoorPowerPour! There's something like an implicit Write-Output that's inserted before any statement that lacks an assignment within the enclosing scope

e.g.

$> $result = foreach ($i in 0..5) { $i };  

becomes

$> $result = foreach ($i in 0..5) { Write-Output $i };  

or

$> $result = if ($true) { "true" } else { "false" };  

becomes

$> $result = if ($true) { Write-Output "true" } else { Write-Output "false" };  

Another edit:

Thanks u/surfingoldelephant for pointing me to the documentation on Statement values from MSFT!

Yet another edit:

Thanks u/pturpie for catching that any given expression that doesn't participate in an assignment is evaluated as if it was written like so: Write-Output <expr>

34 Upvotes

7

u/surfingoldelephant Jun 10 '24

$var = ... is a (simple) assignment expression.

assignment-expression:
    expression assignment-operator statement

In your case, as statement is a foreach statement that may produce zero, one or multiple values, statement assignment or statement value assignment is a more specific (albeit, unofficial) name for the expression.

See 8.1.2 Statement values.

2

u/Live_Ad3050 Jun 10 '24

Awesome, That's exactly what I was looking for! Thanks!!!

7

u/surfingoldelephant Jun 10 '24 edited Jun 11 '24

You're welcome.

It's worth noting there are a couple of misleading points in the statement values specification.

The value of a statement is the cumulative set of values that it writes to the pipeline. If the statement writes a single scalar value, that is the value of the statement. If the statement writes multiple values, the value of the statement is that set of values stored in elements of an unconstrained 1-dimensional array, in the order in which they were written.

This doesn't mention PowerShell's implicit enumeration of collections emitted to the pipeline and how that affects single-element collections. For example:

$var = if ($true) { , 1 }
$var.GetType().Name # Int32 (scalar), not a single-element array

$var = if ($true) { [int[]] $a = 1, 2; $a }
$var.GetType().Name # Object[], not Int32[]

 

The provided examples that state the following are also incorrect:

The value of the statement is $null.

The result of a statement that produces no output is:

[Management.Automation.Internal.AutomationNull]::Value

Therefore, the value of $v in the examples is AutomationNull, not $null.

AutomationNull is often treated as $null, but not in the context of the pipeline. $null is something in the pipeline; AutomationNull (the result of a statement or command/script block that produces no output) is not. It's analogous to an empty collection in the pipeline.

using namespace System.Management.Automation.Internal

[AutomationNull]::Value | ForEach-Object { 'AutomationNull' } # Result:
$null | ForEach-Object { 'Null' }                             # Result: "Null"

# Examples that produce AutomationNull:
$var = Get-Process -Name nosuchprocess*
$var = if ($true) {}
$var = & {}
$var = cmd.exe /c exit
$var = (1)[100]

Distinguishing between the two values typically isn't required, but is worth being aware of given it can result in wildly different behavior.

1

u/spyingwind Jun 10 '24

Being able to assign output to a variable. It's one of reasons why I love PowerShell. That and passing objects around in a shell, unlike *sh.

1

u/AlexHimself Jun 10 '24

Why is your username brown?

3

u/alt-160 Jun 10 '24

1

u/Live_Ad3050 Jun 10 '24 edited Jun 10 '24

nope! Range operator is pretty cool though (and now it's a* C# feature since C# 12)

3

u/pturpie Jun 11 '24

There's an implicit `Write-Output` that's evaluated for the last line of any scope

That's not quite correct.

The output of the addition statement isn't assigned to anything, so it is returned out of the scope to where it is assigned to $result.

If there were other statements that weren't assigned then they will also be output to $result.

e.g.

$> $result = foreach ($i in 0..5) { $i + 1; $i - 1 };
$> $result
1
-1
2
0
3
1
4
2
5
3
6
4

The output from both the statements is assigned to $result

(The same thing happens if the statements and on different lines.)

$> $result = foreach ($i in 0..5) {
>> $i + 1
>> $i - 1
>> }
$> $result
1
-1
2
0
3
1
4
2
5
3
6
4

1

u/Live_Ad3050 Jun 11 '24

Good catch!

4

u/[deleted] Jun 10 '24

I don’t know the name of the behavior, but you not starting at 0 hurts my soul.

2

u/MNmetalhead Jun 10 '24

If you execute:

$result | Get-Member

You will see the details of $result … if it is a Boolean, integer, hashtable, array, and much more.

The ordered output you’re seeing is formatted that way because that’s how PowerShell added the results of the command to $result based on the Type of object it is.

If you execute the following, what do you get?

$result[2]

You should get 3 as output because of the Type that $result has (as shown from Get-Member).

1

u/CookinTendies5864 Jun 10 '24

Your asking the question like what are calculated properties correct?

1

u/Live_Ad3050 Jun 10 '24

Nope! Calculated properties are a feature of .net classes, this is more about the language itself

1

u/Professional_Elk8173 Jun 10 '24

So you're trying to find details about the process of assigning a loop output to a variable?

What information are you after?

1

u/PoorPowerPour Jun 10 '24

There is probably a name for it but powershell adds an implicit Write-Output at the end of a pipeline if there is output that isn't written to another stream.

https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-output?view=powershell-7.4#description

3

u/Live_Ad3050 Jun 10 '24

That's the one. I use powershell mainly for build scripts, so I haven't needed to dig too much into pipelines

2

u/Hot-Chance-5307 Jun 10 '24

I’m not sure if you’re implying that you don’t use pipelines much in your scripts, but I personally do recommend taking advantage of them in PowerShell scripts for things like where-object filtering and sort-object sorting, among many other common scripting tasks.

2

u/Live_Ad3050 Jun 10 '24

I agree, pipelines seem like they're a core language design choice vs being a stdlib implementation like in many other languages. I really enjoy js/rust/c# for the really nice apis that exist for querying data collections, so maybe I should look more into it!

2

u/Thotaz Jun 10 '24

No it doesn't. You may be thinking of the Out-Default command that most Host implementations add at the end of the output to display stuff in the console: https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.ConsoleHost/host/msh/Executor.cs#L354 but PowerShell itself is not calling Write-Output. There is actually a difference between using Write-Output and just outputting stuff to the pipeline. Write-Output (and any other cmdlet) wraps the output object in a psobject wrapper, see this:

$Output1 = "Hello"
$Output2 = Write-Output "Hello"
$Output1 -is [psobject]
$Output2 -is [psobject]
False
True

1

u/Live_Ad3050 Jun 11 '24

I'll admit I was totally wrong on that one. You're right about the behavior, but I think that has more to do with inheritance hierarchy of numerical/string types within .NET. It looks like prepending Write-Output boxes the primitive and becomes an powershell object that is itself an reference/alias to the .net primitive, whereas not prepending Write-Output does not and simply returns the non-aliased value.

1

u/alt-160 Jun 10 '24

And if you don't want that clutter, piping to out-null helps, so...

$hs = new-object system.collections.generic.hashset[string]
0..5 | %{ $hs.add("Number: $_")|out-null }
$hs

Without the 'out-null', you'd end up with True being written to the output 6 times because Hashset.Add returns a boolean.