0

I have an array being returned to me which I am stepping through with the foreach, I want to use one element of each member of that array in a hash table that will be converted to a json object. When I add the element to a powershell array object and then add it to the hash the strings are being combined instead of added as arrays.

Spec

"destinationInterclusterLifIps": [
  "172.31.5.119",
  "172.31.15.103"
]

Actual return:

"destinationInterclusterLifIps":  [
     "172.31.33.150172.31.42.41"
 ],

Building the array

if(!$destinationInterclusterLifIps){
    $destinationInterclusterLifIps =  @()
    foreach($record in $interclusterIpInfo.peerInterClusterLifs){
        $destinationInterclusterLifIps += $record.address
    }
}

Hash table

$body = @{
        destinationInterclusterLifIps = @($destinationInterclusterLifIps)
}

This has to be something simple with hash tables that I am missing

mklement0
  • 312,089
  • 56
  • 508
  • 622
Nicholas Elliott
  • 309
  • 1
  • 2
  • 11
  • 1
    PowerShell's auto-collection-feature-thingy could reduce this to just `$interclusterIpInfo.peerInterClusterLifs.address` without the need for looping. – Jeroen Mostert Mar 22 '19 at 15:40
  • 1
    @JeroenMostert: That thingy doesn't have a name in the official docs, unfortunately; the closest thing we have to an official names is _member enumeration_, in the [blog post that announced the feature in v3](https://blogs.msdn.microsoft.com/powershell/2012/06/13/new-v3-language-features/). It's the name I've been using in my answers here on SO, notably in [this one](https://stackoverflow.com/a/44620191/45375) that tries to explain the feature comprehensively. – mklement0 Mar 22 '19 at 16:10
  • Wasn't aware of that. I'm not a big fan of that name -- it sounds quite ambiguous with, you know, enumerating the individual members, but I suppose it's better than nothing. – Jeroen Mostert Mar 22 '19 at 16:12
  • Agreed on both counts, @JeroenMostert. The docs now at least [describe the behavior](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_properties?view=powershell-6#properties-of-scalar-objects-and-collections) (superficially), but without giving it a name, so at least hypothetically a better name could be chosen. – mklement0 Mar 22 '19 at 16:33
  • On second thought, @JeorenMostert: coming up with names that are both expressive and concise is difficult, and _member enumeration_ isn't half bad: _enumeration_ covers the aspect of operating on all elements of a collection, and _member_ covers what is being accessed on each element. – mklement0 Mar 25 '19 at 21:23

3 Answers3

2

Your code alone doesn't explain the symptom, but your own answer hints at an interesting pitfall that is worth exploring:

  • By default, a PowerShell variable can accept a value of any type, and values of different types can be assigned at any time:

    $var = 42   # $var now contains an [int]
    # ...
    $var = 'hi' # $var now contains a [string]
    
  • However, you can type-constrain variables, in which case they only ever accept values of that type - or values that can automatically be converted to that type (PowerShell is very flexible when it comes to automatic conversions):

    # Create a type-constrained variable.
    [int] $var = 42  # $var now contains an [int] AND is locked into storing [int]s
    
    # Try to assign a [string] that CANNOT be converted to an [int]
    $var = 'hi' # FAILS: 'Cannot convert value "hi" to type "System.Int32"...'
    
    # Try to assign a [string] that CAN be converted to an [int]
    $var = ' 42  ' # OK - string was converted; $var now contains [int] 42
    

Typically, but not necessarily, parameter variables are type-constrained, as part of a script or function's list of declared parameters.

If you later reuse a type-constrained parameter variable by assigning a new value to it, it will enforce its original type, as described above.

The likeliest explanation in your case is that your $destinationInterclusterLifIps was declared as a type-constrained [string] $destinationInterclusterLifIps parameter, in which case a later attempt to assign an array resulted in implicit stringification of that array; to illustrate with a simple example:

function foo {
  param(
   # Type-constrained parameter.
   [string] $destinationInterclusterLifIps
  )

  # ...

  # Try to assign an *array*:
  # Instead of storing an array, the [string] type constraint 
  # converts the array to a *single string*, as a space-separated list of
  # its elements.
  $destinationInterclusterLifIps = 1, 2, 3

  # Output the value.
  $destinationInterclusterLifIps

}

# Call the function (no argument needed)
foo

This outputs:

1 2 3

i.e., the array converted to a string, as a space-separated list of its elements. (An array would print element by element, each on its own line).

Note that your problem was therefore unrelated to the use of hash table and its conversion to JSON - it's just where you happened to notice the problem.


Optional reading: Modifying / removing a variable's type constraint:

It is possible to later recreate a type-constrained variable with a different type; e.g.:

[int] $var = 42; [string] $var = 'hi'

While you can use Remove-Variable to effectively remove a type constraint by removing the variable first and then recreating it unconstrained, as TheIncorrigible1 suggests -
[int] $var = 42; Remove-Variable var; $var = 'hi' - you can use [object] $var = 'hi' as a shortcut, since type-constraining to [object] is tantamount to not constraining the value.

(Get-Variable var).Attributes contains a variable's attributes, and a type-constraint attribute is stored as a [System.Management.Automation.ArgumentTypeConverterAttribute] instance. If you're not concerned about removing all attributes, another simple, though obscure, option for removing a type constraint is to use (Get-Variable var).Attributes.Clear().

mklement0
  • 312,089
  • 56
  • 508
  • 622
  • Thanks. Isn't `[psobject]` preferred for performance as the boxing/unboxing of `[object]` doesn't need to happen? – Maximilian Burszley Mar 22 '19 at 18:53
  • 1
    @TheIncorrigible1: [What is `[psobject]`?](https://github.com/PowerShell/PowerShell/issues/5579). [Never heard of it](https://github.com/PowerShell/PowerShell/issues/5551). But, seriously, I prefer to keep explicit use of `[psobject]` out of my PowerShell code - I wouldn't worry about performance here, but a quick test shows, surprisingly, that using `[psobject]` actually slows things down: `$a=1..1e5; tcm { foreach($i in $a) { [object] $v = 1 } }, { foreach($i in $a) { [psobject] $v = 1 } }`; `tcm` = https://gist.github.com/mklement0/9e1f13978620b09ab2d15da5535d1b27 – mklement0 Mar 22 '19 at 19:02
0

Looks like I was having a conflict with a variable. Maybe because it is being used as a param as well. Setting the variable to any other name cleared the issue.

if(!$sourceInterclusterLifIps){
    $InterclusterIPSource =  @()
    foreach($record in $interclusterIpInfo.interClusterLifs){
        $InterclusterIPSource += $record.address
    }
} else {
    $InterclusterIPSource = $sourceInterclusterLifIps
}
Nicholas Elliott
  • 309
  • 1
  • 2
  • 11
-1

try using an ArrayList with .add(), It should do what you want.

$destinationInterclusterLifIps = New-Object System.Collections.ArrayList
if(!$destinationInterclusterLifIps){
$destinationInterclusterLifIps =  @()
foreach($record in $interclusterIpInfo.peerInterClusterLifs){
    $destinationInterclusterLifIps.add( $record.address )
}

}

Dsabadash
  • 273
  • 1
  • 2
  • 11
  • While this may be more efficient (though it will hardly matter in this case), it neither explains Nicholas' problem nor does it provide a solution, given that it is functionally equivalent to the original code (except for the specific collection type, but that doesn't matter). – mklement0 Mar 22 '19 at 16:56