Who has seen this potential powershell object property treated as method call booby trap? - powershell

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

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'

Related

Array not recognized by powershell parser when other operators are involved

Assigning an array looks like this:
PS> $x = "a", "b"
PS> $x
a
b
Now, i wanted to add a 'root string' ("r") to any element so I did this (actually i used a variable, but for the sakeness of simplicity let's just use a string here):
PS> $x = "r" + "a" , "r" + "b"
PS> $x
ra rb
Looking at the output, I didn't get the array that I expected, but a single string with a "space" (I checked: it's a 32 ascii char, so a space, not a tab or another character).
That is: the comma seems to be interpreted as a string join operator, which I couldn't find any reference to.
Even worst, I get the feeling of not understanding how the parser works here. I had a look at about_Parsing; what I found seems not to apply to this case.
Commas (,) introduce lists passed as arrays, except when the command
to be called is a native application, in which case they are
interpreted as part of the expandable string. Initial, consecutive or
trailing commas are not supported.
The first obvious fix that I came up with is the following:
PS> $x = ("r" + "a") , ("r" + "b")
PS> $x
ra
rb
Maybe there are others, and I am expecially intrested in the ones that reveal how the parser actually works. What I would like to fix the most is my knowledge of the parsing rules.
To flesh out the helpful comments on the answer:
tl;dr
Due to operator precedence, your command is parsed as "r" + ("a" , "r") + "b", causing array "a", "r" to be implicitly stringified to verbatim a r, resulting in two string concatenation operations yielding a single string with verbatim content ra rb.
Using (...) is indeed the correct way to override operator precedence.
"r" + "a" , "r" + "b"
is an expression involving operators.
Expressions are parsed in expression mode, which contrasts with argument mode; the latter applies to commands, i.e. named units of functionality that are called with shell-typical syntax (whitespace-separated arguments, quotes around simple strings optional). Arguments (parameter values) in argument mode are parsed differently from operands in expression mode, as explained in the conceptual about_Parsing help topic. Your quote about , relates to argument mode, not expression mode.
The conceptual about_Operator_Precedence help topic describes the relative precedence among operators, from which you can glean that ,, the array constructor operator has higher precedence than the + operator
Therefore, your expression is parsed as follows (using (...), the grouping operator, to make the implicit rules explicit):
"r" + ("a" , "r") + "b"
+ is polymorphic in PowerShell, and with a [string] instance as the LHS the RHS is coerced to a string too.
Therefore, array "a" , "r" is stringified, which uses PowerShell's custom array stringification, namely joining the (potentially stringified) array elements with a space.[1]
That is, the array stringifies to a string with verbatim content a r.
As an aside: The same stringification is applied in the context of string interpolation via expandable (double-quoted) strings ("..."); that is, "$("a", "r")" also yields verbatim a r
Therefore, the above is equivalent to:
"r" + "a r" + "b"
which yields verbatim ra rb.
(...) is indeed the appropriate way to ensure the desired precedence:
("r" + "a"), ("r" + "b") # -> array 'ra', 'rb'
[1] Space is the default separator character. Technically, you can override it via the $OFS preference variable, though that is rarely used in practice.
Another way to do it. The type of the first term controls what type of operation the plus performs. The first term here is an empty array. If you want the plus to do both kinds of operations, there's no getting around extra parentheses to change the operator precedence.
#() + 'ra' + 'rb'
ra
rb
Or more commonly:
'ra','rb' + 'rc'
ra
rb
rc

Why does Powershell interpret log2 45/120 as log2 "45/120"?

Given a function:
function log2 {param ([double]$x) [Math]::Log($x) / [Math]::Log(2) }
Expected: log2 45/120 should yield -1.41503749928
Actual:
Cannot process argument transformation on parameter 'x'. Cannot convert value "45/120" to type "System.Double".
Error: "Input string was not in a correct format."
At line:1 char:6
+ log2 45/120
+ ~~~~~~
+ CategoryInfo : InvalidData: (:) [log2], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,log2
Why is that?
Uservoice
Powershell doesn't know if 45/100 as an argument to a command is supposed to be an evaluated expression or a string, so it chooses a string. It does that to preserve the argument for the command. It tries to cast the string to a double, and it can't because it's not a numeric string.
For example, I enter this command:
Get-Value 12/19/2017
What should Powershell do with 12/19/2017 before it passes it to Get-Value? The safest answer is nothing. If Get-Value wants to try to convert it, that's not the command line parser's job.
Yes, these are explicit:
Get-Value (12/19/2017)
Get-Value "12/19/2017"
But Powershell needs to have a sensible default that covers everything, and they've chosen to treat all literal argument tokens as a string unless they're designated as an expression with parentheses, a subexpression with $(), or in some cases a script block with {}.
Here's another example. Let's say I want all files with an extension of .000 that start with 4 and end with 1:
Get-ChildItem 4*1.000

Behaviour of PowerShell when Combining HashTables

Question
Why is $null + #{} valid, but #{} + $null not; even where null is cast to a hashtable (#{} + ([hashtable]$null)).
Example Code
[hashtable]$a = #{demo1=1;demo2='two'}
[hashtable]$b = #{demo3=3;demo4='Ivy'}
[hashtable]$c = $null
#combining 2 hashtables creates 1 with both hashes properties (would error if any properties were common to both)
write-verbose 'a + b' -Verbose
($a + $b)
#combining a null hashtable with a non-null hashtable works
write-verbose 'c + a' -Verbose
($c + $a)
#combing 2 null hashtables is fine; even if we've not explicitly cast null as a hashtable
write-verbose 'c + null' -Verbose
($c + $null)
#however, combinging a hashtable with null (i.e. same as second test, only putting null as the right argument instead of the left, produces an error
write-verbose 'a + c' -Verbose
($a + $c)
Output
Name Value
---- -----
demo3 3
demo4 Ivy
demo1 1
demo2 two
VERBOSE: c + a
demo1 1
demo2 two
VERBOSE: c + d
VERBOSE: a + c
A hash table can only be added to another hash table.
At line:19 char:1
+ ($a + $c)
+ ~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : AddHashTableToNonHashTable
Side Note
Incidentally, this led me to discover this useful trick for a null-coalescing operation for hashtables: ($c + #{}). e.g. ($a + ($c + #{})) avoids the error produced above / (($a + #{}) + ($c + #{})) gives us a completely safe way to add hashtables where either value may be null.
I tried to find out exactly why, but I can't for sure.
My original beliefs were:
There may be some PowerShell implicit type coercion happening, where the thing on the right is cast to match the type on the left. e.g. with "1"+1 the 1 on the right becomes a string and the output is "11", but 1+"1" the right becomes a number and the output is 2.
This definitely is not happening, or $null + #{} would either throw a cast error, or cast and do null + null = null, not output an empty hashtable as it does.
Addition of A + B for things other than numbers has some basic rules for things like array concatenation, but beyond that, it will fall down to the .Net Framework underneath and will try to do a method call like A.op_Addition(B) and it will be up to the thing on the left to say what happens when you try to add the thing on the right to it.
This does happen, but is not happening here. Consider #{} + 0 tells you that a hashtable can only be added to a hashtable. (Lies, you can add it to null). And 0 + #{} tells you that hashtables do not contain an op_Addition method. So the addition with null seems like it should give that error, but doesn't, because it's not falling back to this mechanism.
The PSv3 language spec (which I think is the latest version available), download here: https://www.microsoft.com/en-us/download/details.aspx?id=36389, mentions addition:
7.7.1 Addition Description: The result of the addition operator + is the sum of the values designated by the two operands after the usual
arithmetic conversions (§6.15) have been applied.
The usual arithmetic conversions say:
If neither operand designates a value having numeric type, then [..] all operands designating the value $null are converted to zero of type int and the process continues with the numeric conversions listed below.
That implies $null gets converted to 0 in both your examples. Although that cannot be happening because 0 + #{} and #{} + 0 are both errors.
There is an explicit mention of adding of two hashtables in section 7.7:
7.7.4 Hashtable concatenation Description: When both operands designate Hashtables the binary + operator creates a new Hashtable
that contains the elements designated by the left operand followed
immediately by the elements designated by the right operand.
OK, hashtable addition is handled by the PowerShell engine, but only adding two hashtables.
Section 7. introduction says:
Windows PowerShell: If an operation is not defined by PowerShell, the type of the value designated by the left operand is inspected to see if it has a corresponding op_ method.
And the operation $null + Hashtable doesn't seem to be defined by PowerShell from what I can see in the spec, and the corresponding method for + is op_Addition - a method which hashtables do not have, see error code earlier - and this is not throwing that error, and not only that but in the case of adding to 0 the error comes from the Right operand not the left one.
And the other interesting bit in the spec is:
4.1.2 The characteristics of the null type are unspecified.
So the summary appears to be:
#{} + $null - is triggering the PowerShell handling of adding two hashtables, even though the spec doesn't say that it will.
$null + #{} - it looks like there's an implicit $null + x = x rule, although the spec doesn't seem to mention it, and it might be implementation dependent.
[<type>]$null - casting $null to a numeric type results in 0 (6.15 arithmetic conversions), but casting it to anything else(?) appears to result in $null (not in the spec).
The comment and linked chain which says $null has no type are against the PowerShell spec 4.1.2 "the null type" which says "The null type has one instance, the automatic variable $null (§2.3.2.2), also known as the null value. The characteristics of this type are unspecified." so at least in terminology, it's described as a type in PowerShell even if you can't GetType() on it ..

Use PowerShell to divide two decimals

This seems easy, but after searching, I have come up empty-handed.
How can I divide two decimals in powershell?
204.50 / 1,917.75
Throws the following error:
Method invocation failed because [System.Object[]] does not contain a method named 'op_Division'.
At line:1 char:1
+ 204.50 / 1,917.75
+ ~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (op_Division:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
Try leaving the comma out. What you've typed looks like 204.50 divided by 1 and then 917.75
Walter Mitty's answer is an effective solution.
To explain why 204.50 / 1,917.75 failed:
PowerShell number literals (a) always use . as the decimal mark, irrespective of the current culture, and (b) do not support use of a thousands separator (such as ,).
This implies that 1,917.75 is not recognized as a number literal, which begs the question (kinda): how is it parsed?
, is the array-construction operator in PowerShell: that is, tokens separated by , constitute the elements of an array.
Thus, 1,917.75, is the equivalent of #( 1, 917.75 ): a 2-element array containing [int] 1 and [double] 917.75.
, has higher precedence than /, the division operator, so that 204.50 / 1,917.75 is the equivalent of:
204.50 / #( 1, 917.75 )
That is, PowerShell tries to divide [double] literal 204.50 by array #( 1, 917.75 )
Since PowerShell doesn't know how to divide anything by an array (generically represented in PowerShell as [System.Object[]]), you get the following error message:
Method invocation failed because [System.Object[]] does not contain a method named 'op_Division'.
That is, PowerShell looks for a way to apply division operator / (op_Division) to array operands, and since that isn't defined, an error occurs.
As an aside: PowerShell does overload some operators to work with arrays, but only if the array is on the LHS (the left-hand side operand).
Operators -eq / ne, -like / -notlike, -match / -notmatch (do let me know if I'm missing any) accept an array as the LHS and a scalar as the RHS, in which case the operator acts as a filter:
The operator is applied individually to the elements of the LHS (against the scalar on the RHS), and the subset of elements for which the operation returns $true is returned as a subarray of the input array; e.g.:
#( 'donald trump', 'hillary clinton', 'gary johnson' ) -notmatch 'trump'
outputs #( 'hillary clinton', 'gary johnson' ), the subarray of the input containing only those elements that do not contain the substring trump.

Why is it not possible to assign a string to a strongly typed boolean variable?

Let's say I try to assign a string to a strongly typed integer variable:
[int]$bar = '1'
This works, as PowerShell is able to cast the string '1' to an integer.
Now things are different if I attempt the same with a strongly typed boolean variable:
[boolean]$foo = 'foo'
Cannot convert value "System.String" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1
or 0.
I find this confusing, as PowerShell at the same time allows an explicit cast from string to boolean:
[boolean]'foo'
True
Does anyone know the reason for this seemingly inconsistent behavior?
Practical advice:
In most cases I would argue the following approach, rather than typed variables: Convert your values to the target type prior to assignment, and then let the type system infer the variable type:
$foo = [bool]"foo"
$foo = "foo" -as [bool]
$foo = $true -eq "foo"
The problem at hand:
(This is not an authoritative answer, but a best-guess)
Briefly mentioned in the about_Variables help file:
TYPES OF VARIABLES
You can store any type of object in a variable, [...]
Windows PowerShell variables are "loosely typed," which means that
they are not limited to a particular type of object. [...]
[... section about value-inferred types bla bla bla ...]
You can use a type attribute and cast notation to ensure that a
variable can contain only objects of the specified type or objects
that can be converted to that type. If you try to assign a value
of another type, Windows PowerShell tries to convert the value to
its type. If it cannot, the assignment statement fails.
To use cast notation, enter a type name, enclosed in brackets, before
the variable name (on the left side of the assignment statement).
Although "type attribute" and the distinct constraint that this applies only during assignment is used nowhere else in the documentation, but (to me at least) indicates that this is a special case of explicit casting.
What does this imply?
When you add an explicit cast notation to the variable, during assignment, as described above, PowerShell adds an ArgumentTypeConverterAttribute to the variable, and PowerShell's type conversion magic is suddenly overridden by a type-specific Transform() method:
PS C:\> $var = 5
PS C:\> Get-Variable var |fl *
Name : var
Description :
Value : 5
Visibility : Public
Module :
ModuleName :
Options : None
Attributes : {}
PS C:\> [int]$var = 5
PS C:\> Get-Variable var |fl *
Name : var
Description :
Value : 5
Visibility : Public
Module :
ModuleName :
Options : None
Attributes : {System.Management.Automation.ArgumentTypeConverterAttribute}
If we repeat this experiment with a boolean type, you can see how the ArgumentTypeConverterAttribute transform is much more restrictive than the normal conversion "magic":
PS C:\> [bool]$foo = $true
PS C:\> (Get-Variable foo).Attributes[0].Transform($ExecutionContext,"foo")
Exception calling "Transform" with "2" argument(s): "Cannot convert value "System.String" to type "System.Boolean". Boolean parameters
accept only Boolean values and numbers, such as $True, $False, 1 or 0."
At line:1 char:1
+ (Get-Variable foo).Attributes[0].Transform($ExecutionContext,"foo")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentTransformationMetadataException
Again in this type of conversion Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0., whereas powershell itself generally interprets any non-empty, non-zero or non-null value to be $true when implicitly converted to [bool].
in other words:
Cast notations are context-sensitive:
[bool]$SomeVariable = [bool]"bar"
^ ^ ^
| | |
| during assignment |
This is a type attribute|
This is a cast notation
Even though they look just the same