5

Consider the following hashtable:

$table = @{
  6           = '10.11.12.13', '10.11.12.14'
  15          = 'domain.tld'
  NameServers = '10.11.12.13', '10.11.12.14'
}

Normally when you have a hashtable, you can . reference the key name to work with it in a more object-oriented style vs. using the array-accessor syntax [key]. For example, invoking the NameServers key on the hashtable above with a . results in the value being returned, and I can work with members of that property's value as expected:

$table.NameServers       # ========> 10.11.12.13
                         # ========> 10.11.12.14

$table.NameServers.Count # ========> 2

But if I try accessing the 6 key with a . which contains the same string content, it references the correct value fine, but I cannot invoke any members on the returned object. I must use the traditional array-accessor here:

$table.6        # =====> 10.11.12.13
                # =====> 10.11.12.14

$table.6.Count  # =====> ParserError:
                # =====> Line |
                # =====>    1 |  $hi.6.Count
                # =====>      |      ~
                # =====>      | Missing property name after reference operator.

$table[6].Count # =====> 2

Of course, ( $table.6 ).Count works around this issue but this is still a weird quirk of the syntax which I can't explain. Interestingly enough, converting the hashtable to a PSCustomObject yields the same issue. Is this perhaps a parser bug? Or is there something else going on here?

I tested this in Windows PowerShell and PowerShell 7.1 and it happens in both.

codewario
  • 19,553
  • 20
  • 90
  • 159

3 Answers3

6

Note: This answer originally incorrectly claimed that $table.6 doesn't work, due to the hashtable key being integer-typed, but it works just fine, because the 6 in the .6 "property" access is parsed as an integer as well.

As Gyula Kokas' helpful answer points out, you're seeing problematic behavior in PowerShell's parser, present as of PowerShell 7.2, discussed in detail in GitHub issue #14036. The behavior is unrelated to hashtables per se:

  • What follows ., the member-access operator, is parsed as a number if it looks like a number literal, such as 6, 6l (a [long]) or even 6.0(!, a [double]).

    • See below for how that number is then used.
  • Any attempt to use another property access then triggers the error you saw:

    $foo.6.bar # !! Error "Missing property name after reference operator."
    
    • In fact, following a property name that starts with a digit with a letter causes the error too - even though 6a can not interpreted as a number literal.

      $foo.6a # !! Same error.
      

As an aside: PowerShell even allows you to use variable references, and even expressions (enclosed in (...)) as property names (e.g., $propName = 'Length'; 'foo'.$propName or ('foo'.('L' + 'ength')

Workarounds:

  • As you've demonstrated, enclosing the first property access in (...) - ($table.6).Count works, and so does $table.(6).Count

  • You've also demonstrated $table[6].Count, which works for hashtables.

  • For accessing an object's actual property, $obj.'6'.Count would work too (given that property names are always strings), as it would with hashtables with string keys (e.g. @{ '6'= 'six' }.'6'.Length)


Considerations specific to hashtables:

As a syntactic convenience, PowerShell allows property-access syntax ($obj.someProperty) to also be used to access the entries of a hashtable, in which case the property "name" is taken as the key of an entry in the hashtable.

The type-native syntax to access hashtable entries is index syntax ($hash[$someKey]).

While property names (referring to members of a .NET type) are invariably strings:

  • a hashtable's keys can be of any type.
  • on accessing an entry, the lookup key must not only have the right value, but must also be of the exact same type as the key stored in the hashtable.

Perhaps surprisingly, when defining a hashtable, unquoted keys situationally become either strings or numbers:

  • If the unquoted word can be interpreted as a number literal (e.g., 6), it is parsed as such, and you end up with a numeric key; in the case of 6, it will be [int]-typed key, because the usual number-literal typing applies (e.g., 1.0 would become a [double] key (not advisable, because binary floating-point values do not always have exact decimal representations), and 2147483648 would become a [long]).

  • Otherwise (e.g., NameServers), the key is [string]-typed.

    • Caveat: If the name starts with a digit (but isn't a number literal; e.g. 6a), an error occurs; use quoting ('6a') as a workaround - see GitHub issue #15925

That is, even though strings in expressions normally require quoting, for convenience you may omit the quoting when defining keys in hashtable literals - but the rules for recognizing number literals still apply.

Explicitly typing hashtable keys:

  • To ensure that a given key is interpreted as a [string], quote it (e.g., '6')

  • Generally, you may also use a cast to type your keys explicitly. (e.g. [long] 6) or, in number literals, the usual number-type suffix characters, such as L for [long] (e.g. 6L) - see this answer for an overview of the supported suffixes, whose number has grown significantly in PowerShell (Core) 7+.

An example, based on your hashtable:

It follows from the above that your 6 key is of type [int] (and would have to be defined as '6' in your hashtable literal if you wanted it to be a string).

Because the 6 in $name.6 is also parsed as an [int], the lookup succeeds, but note that it wouldn't succeed if different numeric types were at play:

# !! Output is $null, because the entry key is of type [long], 
# !! whereas the lookup key is [int].
@{ 6L = '[long] 6' }.6

Considerations specific to property access:

With actual property access (accessing a native member of a .NET type), the fact that names that look like numbers are actually parsed as numbers first - before, of necessity, being converted to strings - can result in surprising behavior:

# !! Output is $null, because the 6L after "." is parsed as a 
# !! ([long]) number first, and then converted to a string, which
# !! results in "6" being used.
([pscustomobject] @{ '6L' = 'foo' }).6L

# OK - The quoting forces 6L to be a string.
([pscustomobject] @{ '6L' = 'foo' }).'6L' # -> 'foo'
mklement0
  • 382,024
  • 64
  • 607
  • 775
2

I think here is the official issue for your question:

https://github.com/PowerShell/PowerShell/issues/14036

I think the best workaround is $table.(6).count

Gyula Kokas
  • 141
  • 6
2

Hashtables in powershell expect key names to be strings. When you type $table.6 based on your original code the 6 is interpreted as an integer when it's expecting it to be a string (a type conversion feature). The simplest way to resolve this would be to replace 6 with 'six', however you can make this work in a round about way.

If you make the alterations to the code like so:

$table = @{
  '6' = '10.11.12.13', '10.11.12.14'
  '15' = 'domain.tld'
  NameServers = '10.11.12.13', '10.11.12.14'
}

You can reference it by implicitly instructing powershell to treat the 6 as a string by doing the following:

$table.'6'.count
2

It's the same principal as adding keynames with special characters, they have to be instantiated as strings to prevent interpolation or type conversion. For example:

$table = @{'bla $something' = 'anything'}
$table.'bla $something'
anything

In your case I'd do the following and move on to the next problem:

$table = @{
  Six = '10.11.12.13', '10.11.12.14'
  Fifteen = 'domain.tld'
  NameServers = '10.11.12.13', '10.11.12.14'
}

$table.Six.count
2

For further investigation into type conversion follow this link: Powershell Type Conversion Reference

Colyn1337
  • 1,655
  • 2
  • 18
  • 27
  • While explicitly defining the hashtable keys as strings is an option, note that _powershell expect key names to be strings is not correct_ - you can use objects of any type as keys. You are correct about the `6` in `.6` being interpreted as an integer (unlike what I claimed earlier - my apologies), but so is the key if defined _unquoted_, e.g.. `@{ 6 = 'six' }.6` works just fine. Generally, note that _no_ type conversions are involved in hashtable key lookups. The OP's problem is ultimately unrelated to hashtables and is a general _parser bug_. – mklement0 Aug 16 '21 at 21:50