Powershell: Nested member access (dot notation) on a number-like property name fails

Created on 11 Nov 2020  路  6Comments  路  Source: PowerShell/PowerShell

Note: This is a relatively minor issue with an easy workaround, but it would be good to understand if there are other ramifications and a fix would certainly be nice.

Steps to reproduce

([pscustomobject] @{ 1 = 'foo ' }).1 | Should -BeExactly 'foo '
([pscustomobject] @{ 1 = 'foo ' }).1.Trim() | Should -BeExactly 'foo'

Expected behavior

Both tests should succeed.

Actual behavior

The 2nd test fails, because the .1 is apparently no longer recognized as a property access in combination with the .Trim() call:

ParserError:
Line |
   1 |  ([pscustomobject] @{ 1 = 'foo ' }).1.Trim() | Should -BeExactly 'foo'
     |                                     ~
     | Missing property name after reference operator.

Note:

  • If the property name cannot be parsed as a _number_ literal, the problem doesn't surface (e.g., .a1).

  • Workarounds:

    • Quote the property name (thanks, @jantari): ([pscustomobject] @{ 1 = 'foo ' }).'1'.Trim()
    • Enclose the original property access in (...): (([pscustomobject] @{ 1 = 'foo ' }).1).Trim()

      • This workaround also works when accessing _hashtables_ that have actual numeric keys via property syntax, such as with the automatic $Matches variable:



        • $null = 'foo bar' -match '^(foo\s)'; ($Matches.1).Trim()



    • As @BrucePay points out, enclosing just the property name / key in (...) also works in both scenarios:
    • ([pscustomobject] @{ 1 = 'foo ' }).(1).Trim()
    • $null = 'foo bar' -match '^(foo\s)'; $Matches.(1).Trim()

Environment data

PowerShell Core 7.1.0-rc.2
Issue-Question WG-Engine

All 6 comments

Although you're probably aware of it I just want to mention for others who have this issue that this can be worked around for now by quoting the property:

([pscustomobject] @{ 1 = 'foo ' }).1.Trim() # FAILS
([pscustomobject] @{ 1 = 'foo ' }).'1'.Trim() # WORKS

This also works for other property-names that make PowerShell misbehave, such as properties starting with a '#' etc.

Thanks, @jantari - I've added the workaround to the OP. Note that (...) works in all cases, because quoting isn't an option when you access _hashtables_ with truly numeric keys with property syntax, which can happen with the automatic $Matches variable - I've added that to the OP too.

There is actually a lexical ambiguity here. Consider data.1.0. Is this a nested index or indexing with the double 1.0? In practice, we do a shift instead of a reduce so it becomes indexing with a float. I think that with data.1.b, shifting results in an invalid number ultimately resulting in the error you see (I haven't actually checked the code so I don't have the exact details.) Anyway, you can eliminate the ambiguity by putting the number in parens:

([pscustomobject] @{ 1 = 'foo ' }).(1).Trim()

Sort of? Property access tends to convert basically everything to a string in most cases, because most properties can only have strings for the property name. PSObject / PSCustomObject creates objects with string property names, not integer ones. So, yes, that works... incidentally only. $obj.PSObject.Properties.Name.GetType() on that object you create there will tell you the property name is still a string.

There's no reason not to assume a string value is being used for the property name IMO. The only exception is dictionaries/hashtables having non-string key types. Regular objects have strings for property names, I don't think there are really any exceptions to that. It's already true in some cases that you must use $table[$value] to index some kinds of keys, this wouldn't be a new thing.

Agreed, @vexx32:

  • For a non-IDictionary object, it is unequivocally _unhelpful_ to try to parse the member name as a number, given that property names are always strings.

  • For an IDictionary, recognizing [double]s is a feature - even though it's hard to imagine that it would see much use:

PS>  @{ 1.5 = 'foo ' }.1.5.Trim()
foo

If we were to maintain strict backward compatibility, we can't take this feature away.

However, I've never seen a [double]-keyed dictionary in the wild, and the precision issues around binary floating-point representations alone make this an ill-advised thing to do (and the syntax does _not_ work with [decimal]-typed keys).

Given the presumed rarity of [double]-keyed dictionaries and the fact that users will most likely expect _all_ . chars. to be member-access operators, my vote is to consider this a bucket 3 change and always stop parsing a property name / key when a(n unquoted) . is encountered.

  • If there really is a need to use a [double] key, one must then use $dict.(1.5).Trim(), as @BrucePay suggests, or use $dict[1.5], as you suggest.

If preserving backward compatibility is paramount, an alternative that at least ameliorates the problem - if technically feasible - would be to _fall back_ to considering something like 1.b property / key 1, whose b property is to be accessed , after failing to parse 1.b as a [double].

It occurred to me that the parsing in question presumably happens during the _lexical_ analysis phase, where distinguishing by data type being operated on isn't an option. I've revised the previous comment accordingly - again: I think simply not supporting [double] keys with (unquoted, non-expression) property syntax is the best solution.

Was this page helpful?
0 / 5 - 0 ratings