Home > database >  Who has seen this potential powershell object property treated as method call booby trap?
Who has seen this potential powershell object property treated as method call booby trap?

Time:01-18

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

CodePudding user response:

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'
  •  Tags:  
  • Related