PowerShell: Lone assignment statements (including pre/post-increment) don't result in a value in string interpolation

Prerequisites

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

image

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Comments: 21 (3 by maintainers)

Most upvoted comments

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:

    • That something is by design doesn’t necessarily mean that it cannot be changed - but the bar for justifying what then amounts to an invariably breaking change will be higher.
    • In particular, the design intent should be clarified, so that it can be assessed what may be lost / broken in the event of a change.
    • Either way, the impact on backward compatibility must be assessed, and how much existing code there is that relies on the by-design behavior likely corresponds to how intuitively clear / well-documented the design intent is.
    • Key elements of good design are not to contravene users’ intuitive expectations and achieving the design intent with as few rules as possible that users have to remember.
  • 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):

      • 7.1.1 Grouping parentheses specifically spells out for the need for (...) 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.

      • $(...) 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 a foreach 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., in if ($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)").

      • Arguably, this is the primary use case for $(...)
    • $(...) is useful in the following contexts outside "...":

      • To allow the output from a language statement (compound statement) such as foreach and switch 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)
        • Note: For single, non-language statements, (...) is sufficient and preferable; e.g.:
          • Write-Host 'This year:' (Get-Date).Year

it’s not by design

In Windows PowerShell the same behavior. It’s rather by design.

IIRC it was Bruce Payette or Jason Shirk who gave me the trick of $( unpipeable statement ) | .... I think . { unpipeable statement } is functionally the same

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 pipe

Try

.{ for ($index = 0; $index -lt 3; ++$index) { Sleep 1; $index } }

Vs

$(for ($index = 0; $index -lt 3; ++$index) { Sleep 1; $index })

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.

. It’s important to note, esp for @jhoneill too, that "$()" and $() are not the same things at all.

$ 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.

James, one could parenthesise or arrayify the assignment to force an expression interpretation, but this is a workaround for a bug, not what I would say is what powershell users should [have to be] be doing.

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.

PS>  $x = 2

PS>  if (  $x -1 ) {"foo"}
foo

PS>  if ($($x -1)) {"foo"}
foo

PS>  if (  $x -- ) {"foo"}
foo

PS>  $x = 2

PS>  if ($($x --)) {"foo"}

PS>

And it is not just increments or decrements, it’s a all assignments


PS>  if ($s=[datetime]::Now.Second) {"Not the top of a minute"}
Not the top of a minute

PS>  if ($($s=[datetime]::Now.Second)) {"Not the top of a minute"}

PS> 

I’m just saying that string interpolation was simply forgotten about when this existing intended behaviour was implemented, which we can rectify here.

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:

it’s not by design

  • 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.

"$()" and $() are not the same things at all.

"$()" 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:

it passes through the “assigned variable” just isn’t correct.

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.