What are the rules for automatic arrays wrapping/unwrapping? - powershell

Consider this code:
$a = '[{"a":"b"},{"c":"d"}]'
"Test1"
$a | ConvertFrom-Json | ForEach-Object { $_.GetType() }
"Test2"
$b = $a | ConvertFrom-Json
$b | ForEach-Object { $_.GetType() }
This produces the following output:
Test1
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Test2
True False PSCustomObject System.Object
True False PSCustomObject System.Object
Evidently, if we use a temporary variable, whatever is passed down the pipeline is not the same thing that is passed if we do not use one.
I would like to know what the rules that powershell uses for automatic array wrapping / unwrapping are, and if the using a temp var the best course of action if we need to iterate through a json array.
Update 1
Logically ConvertFrom-Json should return an array with the input given and ForEach-Object should iterated on the said array. However in the first test this does not happen. Why?
Update 2
Is it possible that it's ConvertFrom-Json specific? Like bug/issue?

There is only one rule with regard to pipeline items' unwrapping: all arrays and collections written to the pipeline are always getting unwrapped to the items ("unwrapped one level down" or "unwrapped in a non-recursive fashion" would be more correct statement but for the sake of simplicity we are not going to consider nested arrays so far).
There is still a possibility to override this behavior by using unary comma operator:
$a = 1,2
$a | ForEach-Object{ $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
True True Int32 System.ValueType
,$a | ForEach-Object{ $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
In the second case Powershell pipeline engine unwrapped $a but afterwards the result got wrapped back to array by , operator.
As to ConvertFrom-Json case I personally believe that its observed behavior is more predictable as it allows you to capture JSON arrays as a whole by default.
If you are interested in the details, the function Get-WrappedArray in the code below imitates ConvertFrom-Json's behavior:
function Get-WrappedArray {
Begin { $result = #() }
Process { $result += $_ }
End { ,$result }
}
$a | Get-WrappedArray | ForEach-Object{ $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
$b = $a | Get-WrappedArray
$b | ForEach-Object{ $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
True True Int32 System.ValueType

Related

Cast based on variable

I am trying to cast based on the value of a variable, ultimately to be able to actually test to see if $castFrom is of the $castTo type. I can of course do it with a switch like this
$castTo = '[xml]'
$castFrom = #"
<Settings>
<MachineLogFileArchiveFolder></MachineLogFileArchiveFolder>
</Settings>
"#
switch ($castTo) {
'[int]' {
$castResult = [int]$castFrom
}
'[xml]' {
$castResult = [xml]$castFrom
}
}
But that's a little ugly. What I really want too do is something more like this
$castResult = [($castTo)]$castFrom
or this
$castResult = [$($castTo)]$castFrom
but I am getting the impression the switch really is my only option.
Maybe you could use the ToType method:
$castResult = $castFrom.ToType($castTo, [System.Globalization.DateTimeFormatInfo]::CurrentInfo)
Somehow this doesn't work for your example but could be a starting point.
However, you can do this using the Invoke-Expression cmdlet:
$castResult = Invoke-Expression -Command ('[{0}]$castFrom' -f $castTo)
The -as and -is operators just seem simpler for this situation.
$castto = 'string'
$castfrom = 234
$castfrom.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
$castresult = $castfrom -as $castto
$castresult.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
$castfrom -is $castto
False
$castresult -is $castto
True

New-object versus discreet typing with shortcut

I am refactoring some old(ish) code, and I have used two approaches for creating some arrays and ordered dictionaries, for example...
[Collections.Specialized.OrderedDictionary]#{}
and
New-Object Collections.Specialized.OrderedDictionary
I wonder which of these two approaches to instantiating arrays and hashtables/dictionaries is better? FWIW, I need code that that is version agnostic from PS2.0 thru current. Performance is a secondary concern.
FWIW, it looks like the former is MUCH better from a performance standpoint. I tried
Measure-Command {
foreach ($i in 1..10000) {
$array1 = [Collections.Specialized.OrderedDictionary]#{}
}
}
Measure-Command {
foreach ($i in 1..10000) {
$array2 = New-Object Collections.Specialized.OrderedDictionary
}
}
and got 34 milliseconds vs 278 milliseconds. Of course I am not creating 10K instances, nor is performance the main priority, not is 278 milliseconds poor performance even if that was a priority. But it sure shows there is a big difference in the process, even if the end result is actually the same.
When I do the following:
cls
write-host "array1:"
$array1 = [System.Collections.Specialized.OrderedDictionary]#{}
$array1.GetType()
(Measure-Command -Expression {1..10000 | ForEach-Object { $array1.Add($_,"") }}).Milliseconds
write-host
write-host
write-host "array2:"
$array2 = New-Object System.Collections.Specialized.OrderedDictionary
$array2.GetType()
(Measure-Command -Expression {1..10000 | ForEach-Object { $array2.Add($_,"") }}).Milliseconds
I get, e.g., the following, i.e., I think they are the same type so I wouldn't expect any difference, performance or otherwise:
array1:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True OrderedDictionary System.Object
492
array2:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True OrderedDictionary System.Object
500
BTW, as of PS 3.0, $array3=[ordered]#{} gives you the same type+++:
array3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True OrderedDictionary System.Object
+++
About Hash Tables
Beginning in PowerShell 3.0, you can use the [ordered] attribute to
create an ordered dictionary
(System.Collections.Specialized.OrderedDictionary) in PowerShell.

Powershell: How to pipeline array itself instead of its items

How to write function which get array as object from pipeline (actually any object) without to iterate over its items and another one argument (positional). Goal is to modify container (array), but not their items. Something like:
function xxx {
Param( magic-specification??? )
if ($obj type is array) {
iterate over $obj items {
$res = executes $args[0] script-block over them ($_)
if ($res) modify $obj
}
else if ($obj type is object) {
iterate over $obj properties {
$res = executes $args[0] script-block over them ($_)
if ($res) modify $obj property
}
}
here's a demo of how to bypass the way that PoSh is intended to unroll items sent across the pipeline ...
,#(1,2,3) | ForEach-Object {$_.GetType(); "$_" } | Out-Host
#(1,2,3) | ForEach-Object {$_.GetType(); "$_" } | Out-Host
1,2,3 | ForEach-Object {$_.GetType(); "$_" } | Out-Host
output ...
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
1 2 3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
1
True True Int32 System.ValueType
2
True True Int32 System.ValueType
3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
1
True True Int32 System.ValueType
2
True True Int32 System.ValueType
3
note that the 1st one has passed thru an array while the others all unrolled the array. that leading , is the array operator and it causes PoSh to send the array wrapped in another array. the outer one gets unrolled, the inner one gets passed on as an array.

Powershell split by '_' return empty

So i have folder with several files:
$files = #(Get-ChildItem "myPath")
I can see via the debugger that $files contains several items and i want to take the first:
$files[0] = "123_this.is.string"
And i want to split in by '_' and take 123
$splitted = $files[0] -split "_"
So here i can see that $splitted is empty.
Any suggestions why this strange behavior ?
$files[0] isn't a string but a FileSystemInfo Object.
$files[0].getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True FileInfo System.IO.FileSystemInfo
So to get it work you have to use the split function to the filename of the file which is a string.
$files[0].name.getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
With this it should work:
$files[0].name.split("_")
Try:
$files[0].ToString().split("_")

Create an array, hashtable and dictionary?

What is the proper way to create an array, hashtable and dictionary?
$array = [System.Collections.ArrayList]#()
$array.GetType() returns ArrayList, OK.
$hashtable = [System.Collections.Hashtable]
$hashtable.GetType() returns RuntimeType, Not OK.
$dictionary = ?
How to create a dictionary using this .NET way?
What is the difference between dictionary and hashtable? I am not sure when I should use one of them.
The proper way (i.e. the PowerShell way) is:
Array:
> $a = #()
> $a.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Hashtable / Dictionary:
> $h = #{}
> $h.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Hashtable System.Object
The above should suffice for most dictionary-like scenarios, but if you did explicitly want the type from Systems.Collections.Generic, you could initialise like:
> $d = New-Object 'system.collections.generic.dictionary[string,string]'
> $d.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Dictionary`2 System.Object
> $d["foo"] = "bar"
> $d | Format-Table -auto
Key Value
--- -----
foo bar
If you want to initialize an array you can use the following code:
$array = #() # empty array
$array2 = #('one', 'two', 'three') # array with 3 values
If you want to initialize hashtable use the following code:
$hashtable = #{} # empty hashtable
$hashtable2 = #{One='one'; Two='two';Three='three'} # hashtable with 3 values
Hashtable and dictionary in Powershell is pretty much the same, so I suggest using hashtable in almost all cases (unless you need to do something in .NET where Dictionary is required)