2

The following is not an actual question but a cautionary tale about some unexpected PowerShell syntax. The only real question is "Is this behaviour well known to many or only by a few PowerShell developers (i.e. those working ON PowerShell not just WITH PowerShell)?" Note: the examples are only to demonstrate the effect and do not represent meaningful code (no need to ask what the purpose is).

While playing with a PowerShell (5.1.18362.145) switch statement, I received the following error,

PS > $xx = gi somefile
PS > switch ($xx.directory) {
>> $xx.directory{6}
>> }
At line:2 char:17
+ $xx.directory{6}
+                     ~
Missing statement block in switch statement clause.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : MissingSwitchStatementClause

Given previous research on switch, I expected both the $xx.directory expressions to be evaluated and converted to (matching) strings. Clearly {6} would be expected to be the statement clause. Maybe there is some weird parsing happening. Try separating the expression from the statement,

PS > switch ($xx.directory) {
$xx.directory {6}
}
6
PS >

OK, so what happens if we try both,

PS > switch ($xx.directory) {
>> $xx.directory{5} {6}
>> }
Method invocation failed because [System.IO.FileInfo] does not contain a method named 'directory'.
At line:2 char:1
+ $xx.directory{5} {6}
+ ~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : MethodNotFound

What the??? I know braces kinda look like parentheses but what is happening here? Let's try it with an actual method,

PS > 'fred'.substring{1}
Cannot find an overload for "substring" and the argument count: "1".
At line:1 char:1
+ 'fred'.substring{1}
+ ~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodCountCouldNotFindBest

but String.Substring does have an overload with one argument, though it is supposed to be an int. What is this trying to pass? (Hint: what does it look like?) Let's find out,

PS > Add-Type @'
>> public class huh {
>> public void passing(object o)
>> {
>> System.Console.WriteLine(o.GetType().ToString());
>> }
>> }
>> '@
PS > $whatsit=New-Object huh
PS > $whatsit.passing{1}
System.Management.Automation.ScriptBlock
PS >

Who'da thunk it?

About the only other question would be, "Anybody know where this is described in the documentation (assuming it is still happening in 7.2+)?" (Seriously, I'd like to know if it is.)

Uber Kluger
  • 394
  • 1
  • 11
  • 1
    While this problem is both interesting and on-topic (and has a simple but annoying explanation), and the content in your post is great, please follow the normal convention: Either formulate a question that includes reproducible code and appropriately describes the problem/symptoms you're experiencing, _then post a self-answer below_, or (if you're seeking further explanation about this behavior), please reformat your post to explicitly ask "what's happening here?" instead of "Who'da thunk it" :) – Mathias R. Jessen Jan 13 '22 at 15:31
  • 1
    Although buried deep in the post, there was a question "Is this documented and, if so, where?" This was fully answered by @mklement0 (and now marked as such). As for "Who'da thunk it?', this wasn't a real question, just an (over) colloquial way of commenting how it turned out from the test that something that looked like a script block was, in fact, a script block. (I already knew by then hence the hint.) – Uber Kluger Jan 17 '22 at 15:33

1 Answers1

3

As - perhaps unfortunate - syntactic sugar, PowerShell allows you to shorten:

$object.Method({ ... })

to:

$object.Method{ ... }

Note:

  • In both cases there mustn't be a space after the method name (whereas C# allows "foo".Substring (1), for instance).

  • Method in the above example merely has to be syntactically valid as a method name in order for both expressions to be treated as method calls - a method call is attempted even if no such method exists or if the name happens to refer to a property instead.

In other words:

  • Methods that accept exactly one (non-optional) argument of type script block ([scriptblock]; { ... }) allow invocation without parentheses ((...)).

Arguably, such a narrow use case wouldn't have called for syntactic sugar:

  • Limiting support to script blocks limits the syntactic sugar to PowerShell-provided/-targeted types and their methods, given that script blocks are a PowerShell-specific feature.

  • The requirement not to separate the name and the opening { with a space is at odds with how script blocks are customarily passed to cmdlets (e.g. 1, 2, 3 | ForEach-Object { $_ + 1 } vs. (1, 2, 3).ForEach{ $_ + 1 } - see below)

  • Having to switch back to (...) as soon as two or more arguments must be passed is awkward.

Presumably, this was introduced to cut down on the "syntactic noise" of one common scenario: the use of the PSv4+ .ForEach() and .Where() array methods, introduced for the DSC (Desired State Configuration) feature, which are typically invoked with only a script block; e.g.:

  • (1, 2, 3).ForEach({ $_ + 1 }) can be simplified to (1, 2, 3).ForEach{ $_ + 1 }

As for documentation:

  • The behavior is described only in the context of the aforementioned .ForEach() and .Where() methods in the context of the conceptual about_Arrays help topic:

The syntax requires the usage of a script block. Parentheses are optional if the scriptblock is the only parameter. Also, there must not be a space between the method and the opening parenthesis or brace.

  • Given that it applies to any method with the appropriate signature (irrespective of the .NET type and irrespective of whether it is an instance or a static method), it should arguably (also) be documented in the conceptual about_Methods help topic, which isn't the case as of this writing.

Even with coverage in about_Methods, however, the challenge is to even infer that a method call is being attempted when you don't expect that - at least in the switch case the error message doesn't help.


Design musings:

Note that the .ForEach() and .Where() methods are generally a somewhat awkward fit, given that most native PowerShell features do not rely on methods, and that even regular method-invocation syntax can cause confusion with the shell-like syntax used to invoke all other commands (cmdlets, functions, scripts, external programs) - e.g., $foo.Get('foo', bar') vs. Get-Foo foo bar.

This rejected RFC on GitHub proposed introducing -foreach and -where operators, which would have allowed the following, more PowerShell-idiomatic syntax:

  • 1, 2, 3 -foreach { $_ + 1 }
  • 1, 2, 3, 2 -where { $_ -eq 2 }, 'First'
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Do you think this is worth raising as an issue for? The parsing logic is a bit _too_ simplistic here IMO - PowerShell indiscriminately produces an invocation expression if a single block-token follows a member name without whitespace in between and no further validation/lowering occurs as far as I can tell. We could probably exclude this behavior in certain contexts (like when the expression's immediate parent is a `switch` body!) – Mathias R. Jessen Jan 13 '22 at 16:52
  • @MathiasR.Jessen, on second thought: I've removed my proposal, because it would have required modifying the _parsing_ behavior based on information that isn't available until _runtime_ (whether a given member name refers to an actual method). In the end I think that properly documenting the current behavior is the best option - the behavior is obscure, but it's predictable. The `switch` example is unfortunate, due to how the error surfaces, but it strike me as quite an exotic scenario. – mklement0 Jan 14 '22 at 15:21
  • I considered that too, but method invocation expressions are never invoked correctly when supplied as static/stand-alone test cases anyway: https://gist.github.com/IISResetMe/a0e455507c73824891ebff64d746bd4c - I do agree it _is_ quite exotic, and the "wiring" would be a bit clunky, but we wouldn't be excluding/breaking any already-existing use cases at least :) – Mathias R. Jessen Jan 14 '22 at 15:42
  • 1
    @MathiasR.Jessen, such method calls _do_ work, but their results are matched with implied `-eq`, as usual; change `switch (1)` to `switch ($true)` in your Gist to see that it works; another example: `switch (3) { 'foo'.Length { 'three' } }` – mklement0 Jan 14 '22 at 16:24
  • 1
    Nice catch, didn't consider that. I guess we better leave it be then :) – Mathias R. Jessen Jan 14 '22 at 16:28
  • ***Syntactic sugar?*** When did eliminating the parentheses from an **actual** method call become desirable? Much of language development for decades now has been about reducing ambiguity in the grammar or runtime behaviour. While the implementation of `.where` and `.foreach` might not be an actual method call of a collection object, it certainly *looks* like one. As for `-foreach` and `-where` operators, does the name of the `.where()` mode enumeration not indicate something about the real intent - `WhereOperatorSelectionMode`? (Comment continues...) – Uber Kluger Jan 17 '22 at 13:44
  • Further, I *suspect* that these **are** being treated as operators that expect an array as the RHS. An isolated script block is being treated like any other non-collection object in an array context. Why no space permitted between `ForEach` and `{`? So the *parser* doesn't start a new expression but treats `.ForEach{` as a method call. Why does it need parentheses for extra parameters? Because the `.` dereferencing operator has higher precedence than `,` in the parser. Why use functional instead of operator syntax? Maybe so it looks like ECMA-262 `Array.prototype.forEach`. (Continued again...) – Uber Kluger Jan 17 '22 at 14:17
  • @UberKluger, yes, syntactic sugar. You may think this particular sugar is a bitter pill to swallow - and, if it hasn't yet become clear from my answer, I fully agree - but it is still syntactic sugar. Yes, `.where` and `.foreach` are syntactically method calls - it's just that they happen to be [_intrinsic_ methods](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Intrinsic_Members). Yes, that in _argument mode_ with the `ForEach-Object` and `Where-Object` _cmdlets_ you don't need a space before the `{` is an unfortunate asymmetry. – mklement0 Jan 17 '22 at 14:25
  • Some PowerShell syntax appears to be designed to allow PowerShell to look like other languages. A previous debate about "Why does ( start a new argument in a command line even in the middle of a string?" was answered for me when I saw the following invocation of `New-Object` in several posts (varying locations, generic example) `New-Object IO.FileStream($filename, [IO.FileAccess]::Read, [IO.FileShare]::Read)`, i.e. this behaviour allows PowerShell to *look* like C# (or VB or ECMA-262 or whatever), as in `new System.IO.FileStream(filename, System.IO.FileAccess.Read, System.IO.FileShare.Read)`. – Uber Kluger Jan 17 '22 at 14:37
  • @mklement0 Sorry, the last few comments (by me) were slightly off topic. I was trying to give a possible *motivation* for this syntax, not complain about it. If you create a "language", it looks the way you want. The **real** topic was always the documentation. Frankly, your suggestion of `-foreach` and `-where` operators makes a lot of sense and should be implemented. `.ForEach` and `.Where` are far too baked in to remove but they could be deprecated (or marked as such with no intention to delete) and their use discouraged, simply to avoid new scripts which utilise this syntactic kludge. – Uber Kluger Jan 17 '22 at 14:58
  • Thanks, @UberKluger. Your `New-Object` example is a very common anti-pattern - _pseudo method syntax_ - which PowerShell unfortunately _allows_, but I think that is more accidental than by design. For argument lists directly following a _command name_, you can even let `Set-StrictMode -Version 2` and above catch it, but notably only with _two or more_ arguments (and, since in the `New-Object` case the treacherous syntax is applied to an _argument_ that wouldn't help) - see [this answer](https://stackoverflow.com/a/50636061/45375). Hear, hear re `-foreach` and `-where` - fully agree. – mklement0 Jan 17 '22 at 15:01