How do I convert a powershell hashtable to an object? - powershell

Some hashtables in PowerShell, such as those imported with Import-PowerShellDataFile, would be much easier to navigate if being a PSCustomObject instead.
#{
AllNodes = #(
#{
NodeName = 'SRV1'
Role = 'Application'
RunCentralAdmin = $true
},
#{
NodeName = 'SRV2'
Role = 'DistributedCache'
RunCentralAdmin = $true
},
#{
NodeName = 'SRV3'
Role = 'WebFrontEnd'
PSDscAllowDomainUser = $true
PSDscAllowPlainTextPassword = $true
CertificateFolder = '\\mediasrv\Media'
},
#{
NodeName = 'SRV4'
Role = 'Search'
},
#{
NodeName = '*'
DatabaseServer = 'sql1'
FarmConfigDatabaseName = '__FarmConfig'
FarmContentDatabaseName = '__FarmContent'
CentralAdministrationPort = 1234
RunCentralAdmin = $false
}
);
NonNodeData = #{
Comment = 'No comment'
}
}
When imported it will become a hashtable of hashtables
$psdnode = Import-PowerShellDataFile .\nodefile.psd1
$psdnode
Name Value
---- -----
AllNodes {System.Collections.Hashtable, System.Collect...
NonNodeData {Comment}
$psdnode.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Hashtable System.Object
and the data structure will be just weird when navigating by property name.

There's good information in the existing answers, but given your question's generic title, let me try a systematic overview:
You do not need to convert a hashtable to a [pscustomobject] instance in order to use dot notation to drill down into its entries (properties), as discussed in the comments and demonstrated in iRon's answer.
A simple example:
#{ top = #{ nested = 'foo' } }.top.nested # -> 'foo'
See this answer for more information.
In fact, when possible, use of hashtables is preferable to [pscustomobject]s, because:
they are lighter-weight than [pscustomobject] instances (use less memory)
it is easier to construct them iteratively and modify them.
Note:
The above doesn't just apply to the [hashtable] type, but more generally to instances of types that implement the [System.Collections.IDictionary] interface or its generic counterpart, System.Collections.Generic.IDictionary[TKey, TValue]], notably including ordered hashtables (which are instances of type System.String, which PowerShell allows you to construct with syntactic sugar [ordered] #{ ... }).
Unless noted, hashtable in the following section refers to all such types.
In cases where you do need to convert a [hasthable] to a [pscustomobject]:
While many standard cmdlets accept [hasthable]s interchangeably with [pscustomobjects]s, some do not, notably ConvertTo-Csv and Export-Csv (see GitHub issue #10999 for a feature request to change that); in such cases, conversion to [pscustomobject] is a must.
Caveat: Hasthables can have keys of any type, whereas conversion to [pscustomobject] invariably requires using string "keys", i.e. property names. Thus, not all hashtables can be faithfully or meaningfully converted to [pscustomobject]s.
Converting non-nested hashtables to [pscustomobject]:
The syntactic sugar PowerShell offers for [pscustomobject] literals (e.g., [pscustomobject] #{ foo = 'bar'; baz = 42 }) also works via preexisting hash; e.g.:
$hash = #{ foo = 'bar'; baz = 42 }
$custObj = [pscustomobject] $hash # Simply cast to [pscustomobject]
Converting nested hashtables, i.e. an object graph, to a [pscustomobject] graph:
A simple, though limited and potentially expensive solution is the one shown in your own answer: Convert the hashtable to JSON with ConvertTo-Json, then reconvert the resulting JSON into a [pscustomobject] graph with ConvertFrom-Json.
Performance aside, the fundamental limitation of this approach is that type fidelity may be lost, given that JSON supports only a few data types. While not a concern with a hashtable read via Import-PowerShellDataFile, a given hashtable may contain instances of types that have no meaningful representation in JSON.
You can overcome this limitation with a custom conversion function, ConvertFrom-HashTable (source code below); e.g. (inspect the result with Format-Custom -InputObject $custObj):
$hash = #{ foo = 'bar'; baz = #{ quux = 42 } } # nested hashtable
$custObj = $hash | ConvertFrom-HashTable # convert to [pscustomobject] graph
ConvertFrom-HashTable source code:
Note: Despite the name, the function generally supports instance of types that implement IDictionary as input.
function ConvertFrom-HashTable {
param(
[Parameter(Mandatory, ValueFromPipeline)]
[System.Collections.IDictionary] $HashTable
)
process {
$oht = [ordered] #{} # Aux. ordered hashtable for collecting property values.
foreach ($entry in $HashTable.GetEnumerator()) {
if ($entry.Value -is [System.Collections.IDictionary]) { # Nested dictionary? Recurse.
$oht[$entry.Key] = ConvertFrom-HashTable -HashTable $entry.Value
} else { # Copy value as-is.
$oht[$entry.Key] = $entry.Value
}
}
[pscustomobject] $oht # Convert to [pscustomobject] and output.
}
}

What is the issue/question?
#'
#{
AllNodes = #(
#{
NodeName = 'SRV1'
Role = 'Application'
RunCentralAdmin = $true
},
#{
NodeName = 'SRV2'
Role = 'DistributedCache'
RunCentralAdmin = $true
},
#{
NodeName = 'SRV3'
Role = 'WebFrontEnd'
PSDscAllowDomainUser = $true
PSDscAllowPlainTextPassword = $true
CertificateFolder = '\\mediasrv\Media'
},
#{
NodeName = 'SRV4'
Role = 'Search'
},
#{
NodeName = '*'
DatabaseServer = 'sql1'
FarmConfigDatabaseName = '__FarmConfig'
FarmContentDatabaseName = '__FarmContent'
CentralAdministrationPort = 1234
RunCentralAdmin = $false
}
);
NonNodeData = #{
Comment = 'No comment'
}
}
'# |Set-Content .\nodes.psd1
$psdnode = Import-PowerShellDataFile .\nodefile.psd1
$psdnode
Name Value
---- -----
NonNodeData {Comment}
AllNodes {SRV1, SRV2, SRV3, SRV4…}
$psdnode.AllNodes.where{ $_.NodeName -eq 'SRV3' }.Role
WebFrontEnd

A very simple way, that I discovered just yesterday, is to do a "double-convert" over JSON.
$nodes = Import-PowerShellDataFile .\nodes.psd1 | ConvertTo-Json | ConvertFrom-Json
$nodes
AllNodes
--------
{#{NodeName=SRV1; RunCentralAdmin=True; Role=Application}, #{NodeName=SRV2; RunCentralAdm...}
$nodes.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object

Related

Powershell convert Array of Objects into PSCustomObject

I would like to convert this array of objects
Name CIDR
---- ----
sdc-MO 10.92.18.136/20
sdc-RM 10.77.6.34/20
into a single [PSCustomObject]
sdc-MO sdc-RM
------- -------
{10.92.18.136/20} {10.77.6.34/20}
Please suggest any easy way.. Thanks
Add each object to a hashtable or other dictionary dictionary type, then use the dictionary to create the object (each entry will become a separate property):
$array = #(
[pscustomobject]#{ Name = 'sdc-MO'; CIDR = '10.92.18.136/20' }
[pscustomobject]#{ Name = 'sdc-RM'; CIDR = '10.77.6.34/20' }
)
# Prepare a new dictionary to hold the properties
$newProperties = [ordered]#{}
foreach($inputObject in $array){
# If we don't already have a property with the given name,
# create a new entry in the dictionary
if(-not $newProperties.Contains($inputObject.Name)){
$newProperties.Add($inputObject.Name, #())
}
# Add the `CIDR` value to the corresponding property name
$newProperties[$inputObject.Name] += $inputObject.CIDR
}
$newObject = [pscustomobject]$newProperties
$newObject will be like what you described in the question:
PS C:\> $newObject
sdc-MO sdc-RM
------ ------
{10.92.18.136/20} {10.77.6.34/20}

How to remove item from an array in PowerShell (v5.1)

I have an array of elements in PowerShell (v5.1) as below.
I want to remove the item with matching condition EmpId=102
$testData = #(
#{ Name='ABC'; EmpId=100 },
#{ Name='EFG'; EmpId=101 },
#{ Name='XYZ'; EmpId=102 }
);
Is there any simple way to achieve this?
maybe something like $testData.Remove($testData.Find("EmpId == 102")) ?
if your array is really a collection of one-item hashtables, then the following otta work ... [grin] it pipes the collection thru the Where-Object cmdlet & filters out anything item that has the value-to-exclude in the EmpId key value.
$TestData = #(
#{ Name='ABC'; EmpId=100 }
#{ Name='EFG'; EmpId=101 }
#{ Name='XYZ'; EmpId=102 }
)
$EmpId_ToExclude = 102
$TestData |
Where-Object {
$_['EmpId'] -ne $EmpId_ToExclude
}
output ...
Name Value
---- -----
EmpId 100
Name ABC
EmpId 101
Name EFG
note that the items are not required to be in order ... that is how hashtables work if you don't specify [ordered].
I hope this info is some use. Maybe you can use an arraylist. You don't need #( ) to make an array.
[collections.arraylist]$testData = #{ Name='ABC'; EmpId=100 },
#{ Name='EFG'; EmpId=101 },
#{ Name='XYZ'; EmpId=102 }
$a = $testdata[1]
$testdata.remove($a)
Or
$testdata.removeat(1)
Unfortunely, this doesn't work. I guess you would need a pointer to the hashtable:
$testdata.remove(#{ Name='XYZ'; EmpId=102 })
Actually, it would be easy with a single hashtable:
$testData = #{ 'ABC'=100
'EFG'=101
'XYZ'=102 }
$testdata.remove('xyz')
$testdata
Name Value
---- -----
ABC 100
EFG 101

Strongly type PS custom object properties

I have been using hash tables to return complex data from functions, and it has worked well, but I would like to have the keys strongly typed, since I have booleans, strings, arrays of strings, ordered dictionaries and such in the returned hash tables. So, given something like this
[hashtable]$hashtable = #{
one = 1
two = "two"
}
I have the issue that the type of each key is weakly typed. I want to basically do this
[hashtable]$hashtable = #{
[int]one = 1
[string]two = "two"
}
But that's not valid code. So I thought I could do this
[psCustomObject]$object = [psCustomObject]#{
[int]one = 1
[string]two = "two"
}
But that's invalid too. I find this a bit ugly, and it also doesn't work
$object = New-Object -typeName:PSObject
$object | Add-Member -memberType:int -name:'one' -value:1
$object | Add-Member -memberType:string -name:'two' -value:'two'
So, am I SOL and there is no way, or no elegant way, to create a custom object with strongly typed properties?
Inside the hashtable literal you'll want to type-cast the value expression instead:
PS C:\> $object = [PSCustomObject]#{
one = [int]1
two = [string]"two"
}
PS C:\> $object|gm -MemberType NoteProperty
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
one NoteProperty int one=1
two NoteProperty string two=two
This will, however, not prevent anyone from storing a non-integer or non-string in any of the properties - psobject property are simply not strongly typed.
If you want type safety for properties you'll need to create a new type with the class keyword:
class MyOneTwo
{
[int]$One
[string]$Two
MyOneTwo(){}
MyOneTwo([int]$one, [string]$two){
$this.One = $one
$this.Two = $two
}
}
# Create instances with ::new(), New-Object or a cast:
$object = [MyOneTwo]::new(1,"2")
$object = New-Object MyOneTwo -Property #{ One = 1; Two = "2" }
$object = [MyOneTwo]#{ One = 1; Two = "2" }

Hashtable Parameter Acting Like Reference Variable

It seems like if you pass a hashtable as a parameter to a function and modify the variable that the function has created for that hashtable, the original variable gets modified too. So the hashtable paramater is acting like a reference variable. Why is this?
Example that will probably explain this better:
function testParams ([hashtable]$hashParam, [string]$strParam) {
$hashParam.Remove('a')
$strParam = "I am a string"
}
$str = "a string"
$ht = #{}
$ht['a'] = 'aaaa'
$ht['b'] = 'bbbb'
$ht['c'] = 'cccc'
testParams $ht $string
Write-Host $str
Write-Host "$($ht | Out-String)"
...And that will output:
a string
Name Value
---- -----
c cccc
b bbbb
Edit: Just found a simpler example:
$ht = #{}
$ht[1] = '1111'
$ht[2] = '2222'
$htCopy = $ht
$htCopy.Remove(1)
$ht
Which would output:
Name Value
---- -----
2 2222

Change the type name of a Powershell object from PSCustomObject to something I choose?

I have a script that uses custom objects. I create them with a pseudo-constructor like this:
function New-TestResult
{
$trProps = #{
name = "";
repo = #{};
vcs = $Skipped;
clean = New-StageResult; # This is another pseudo-constructor
build = New-StageResult; # for another custom object.
test = New-StageResult; # - Micah
start = get-date;
finish = get-date;
}
$testResult = New-Object PSObject -Property $trProps
return $testResult
}
These are useful because they can be passed to something like ConvertTo-Csv or ConvertTo-Html (unlike, say, a hashtable, which would otherwise accomplish my goals). They are typed as PSCustomObject objects. This code:
$tr = new-testresult
$tr.gettype()
returns this:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object
Can I change the Name field returned there from PSCustomObject to something else?
Later on when I'm collating test results, I'll pass to another function what will sometimes be an individual result, and sometimes an array of results. I need to be able to do something different depending on which of those I get.
Any help is appreciated.
Sure, try this after creating $testResult:
$testResult.psobject.TypeNames.Insert(0, "MyType")
The heart of the PowerShell extended type system is the psobject wrapper (at least in V1 and V2). This wrapper allows you to add properties and methods, modify type names list and get at the underlying .NET object e.g.:
C:\PS > $obj = new-object psobject
C:\PS > $obj.psobject
BaseObject :
Members : {string ToString(), bool Equals(System.Object obj), int GetHashCode(), type GetType()}
Properties : {}
Methods : {string ToString(), bool Equals(System.Object obj), int GetHashCode(), type GetType()}
ImmediateBaseObject :
TypeNames : {System.Management.Automation.PSCustomObject, System.Object}
Or try this from the prompt:
C:\PS> $d = [DateTime]::Now
C:\PS> $d.psobject
...
I created a special cmdlet to detect the type name of the object under the powershell quickly.
For custom objects,.getType() Method cannot get the ETS type name.
function Get-PsTypeName {
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true,
ValueFromPipeline = $true)]
$InputObject
)
begin {
}
process {
((Get-Member -InputObject $InputObject)[0].TypeName)
}
end {
}
}