PowerShell: Lone assignment statements (including pre/post-increment) don't result in a value in string interpolation
Prerequisites
- Write a descriptive title.
- Make sure you are able to repro it on the latest released version
- Search the existing issues.
- Refer to the FAQ.
- Refer to Differences between Windows PowerShell 5.1 and PowerShell.
Steps to reproduce
Create a variable, attempt to insert it into a string whilst acting upon it (=
, ++
or --
).
Expected behavior
PS> $bob = 7; "_$(++$bob)_"
_8_
Actual behavior
PS> $bob = 7; "_$(++$bob)_"
__
#$bob is still successfully set to `8`, though
Error details
No response
Environment data
Name Value
---- -----
PSVersion 7.4.0-preview.4
PSEdition Core
GitCommitId 7.4.0-preview.4
OS Debian GNU/Linux…
Platform Unix
PSCompatibleVersions {1.0, 2.0, 3.0, …
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Visuals
About this issue
- Original URL
- State: open
- Created a year ago
- Comments: 21 (3 by maintainers)
To provide as much context as I can think of (without - for now - weighing in on whether a change is warranted at all, and, if so, which):
On a meta note:
As for whether the behavior is by design:
Turns out that it is by design, according to the language specification (which dates back to v3 and is the latest one available):
(...)
enclosure inside$(...)
for a top-level expression with side effects (meaning assignment operations, including increment and decrement) in order to produce output - search for"taking place in a string literal"
to find the specific example.The - simple - rules spelled out above are therefore consistent with this design intent; to recap:
$(...)
produces the same output as...
; if$(...)
happens to be placed inside"..."
, whatever output there is is stringified; if there is no output, the result is the empty string....
outputs “nothing”, i.e. an “enumerable null” (“Automation null”, i.e.[System.Management.Automation.Internal.AutomationNull]::Value)
,$(...)
outputs a genuine$null
.$(...)
encloses one or more statements; a statement exclusively comprising a side-effect-producing assignment expression such as++$bob
(or$bob += 1
or$bob = 42
) has no output; hence$(++$bob)
has no output.Since enclosing a top-level assignment operation in
(...)
is required in order to produce (pipeline) output, something like$((++$bob))
is therefore needed.When does a top-level assignment operation implicitly output a value?
Only in conditionals of language statements such as
if
,while
,switch
, … and even in aforeach
loop’s input specification (e.g.foreach ($i in $j = 42) { $i }
- this is the de-facto behavior; I’m not sure where that is documented; it is not spelled out in the Statements chapter.With the exception of
foreach
, the syntactically required presence of(...)
(e.g., inif ($files = Get-ChildItem *.txt) { ... }
) can serve as a visual reminder of this behavior.In what contexts is
$(...)
useful / required?$(...)
is required inside"..."
(interpolating aka expandable strings) in order to embed anything that goes beyond merely referencing a variable (value) as a whole (e.g."$var"
vs."$($var.Property)"
).$(...)
$(...)
is useful in the following contexts outside"..."
:To allow the output from a language statement (compound statement) such as
foreach
andswitch
to act as pipeline input:As noted, using
. { ... }
or& { ... }
is the preferred alternative for providing the output from...
in a streaming fashion (rather than the collect-all-output-first semantics implied by$(...)
)However, the
$(...)
approach may come into play:In cases where collect-all-output-first semantics is explicitly desired, such as to collect input to a subsequent command in full, up front, so as to ensure that processing in the subsequent command cannot interfere with input collection.
In existing code written by users unaware of the
. { ... }
/& { ... }
alternative.See also: #6817
To allow the output from either a language statement and/or or from multiple statements to act as command arguments; e.g. (somewhat contrived):
Write-Host 'This year and last year:' $((Get-Date).Year; (Get-Date).AddYears(-1).Year)
(...)
is sufficient and preferable; e.g.:Write-Host 'This year:' (Get-Date).Year
In Windows PowerShell the same behavior. It’s rather by design.
Well let me give you this one! It’s quite actually different, say your for loop is network bound and takes seconds to execute, and/or your processing on the other end of the pipe is cpu intensive and takes a while to chug through each one,
.{}
will go way faster as$()
waits for the internal statement to complete, then tries to feed all the data at once down the pipeTry
Vs
Not only that but if it produces a lot of data the overhead of that alone I’ve seen slow down powershell as opposed to streaming it as soon as it comes through.
$ basically says “value of”
$Name
is the value of the variable “name” and$( statement )
is the value(s) returned by the statement. This is true inside or outside quotes.So this
for ($i = 0 ; $i -lt 5 ; $i ++) {$i} | where {$_ % 2}
gives a syntax error. A for statement (unlike the foreach cmdlet) can’t be piped. But for$( for ($i = 0 ; $i -lt 5 ; $i ++) {$i} ) | where {$_ % 2}
says “take the values from the for statement and pipe those” which does work and if we wrap it in double quotes"My numbers are $( for ($i = 0 ; $i -lt 5 ; $i ++) {$i} ) "
the evaluated statement works .Many people don’t realise that this parses
$PSVersionTable. PSVersion
- Powershell allows space after the.
when used as a member operator. (.
at the start of a line or in, for example $x = . .\profile.ps1 is the call_in_current_scope operator). That gives a problem for string interpolation, and how to parse"$PSVersionTable.PSversion"
and the implementation goes for simple parsing $ followed by alphanumerics treats those as a variable name and $( ) works as outside.Totally agree. It’s a work round BUT I’m also advancing that to support that this is a long standing accident of implementation, not a deliberate design.
Here’s another case.
And it is not just increments or decrements, it’s a all assignments
I don’t think that is the case - I think string interpolation is surfacing a problem that exists elsewhere - the difference being that outside strings the writing of
$()
is usually optional.Nah that’s cool, my bad too. Online text chats can get argumentative quickly, tone is like impossible to gauge 😕 yeah I guess my only point would be that doesnt necessarily make it by design, potentially unnoticed all this time
I just meant that this behavior is from the first versions of PowerShell, which means it’s by design. I also sorry for not making my point clear.
@Hashbrown777:
Unless you’ve designed it or can point to documentation explicating the design, you don’t know whether it is by design, and you shouldn’t make definitive claims.
This is why I said I think it’s by design, and I’ve given a potential rationale - which, as far as I can tell, offers a straightforward conceptualization.
"$()"
is$()
with stringification, as I’ve suggested.And conceptualizing it as that makes the most sense to me.
Changing the fundamental behavior of
$()
just because it happens to occur in the context of string interpolation seems ill-advised.As an - inconsequential - aside:
That’s probably because I never said that.
What I did say was: “causes the assigned value to be passed through” - which, indeed, is typically, but not always correct - namely not in the post-increment/decrement case, hence my caveat to clarify this exception.
Update: For the full picture, see the footnote in my previous comment.