Videos
I've done plenty of 2-D arrays as well as 1-D hashes but now I seem to have confused myself in trying to do a 'simple' 2-D hash. Maybe I'm mistaken in thinking this counts as 2-D?
MY GOAL
I am trying to create a hash, e.g. for a list of foods, where each food-item (row) would contain multiple properties (columns), e.g.
Food Calories Fat Price ----- -------- ----- ----- ham 100 10 1.00 eggs 200 20 2.00
How do I initialize this hash, how do I add new rows, and how do I recall a particular row (by Food label) and edit the associated values?
Thank you for any suggestions!
why is it when i declare as hashtable, I can access its properties like an object?
PS C:\Users\john> $obj = @{
>> Name = "John"
>> Age = 30
>> }
PS C:\Users\john> $obj.Name
Johnis this just syntactical sugar, or something? thought i would have to do this:
$obj[Name]
You basically want a hash table with values that are arrays. You don't have to use $hashtable.get_item or .add
$myHashTable = @{} # creates hash table
$myHashTable.Entry1 = @() #adds an array
$myHashTable.Entry1 += "element1"
$myHashTable.Entry1 += "element2"
This results in the following output:
$myHashTable
Name Value
---- -----
Entry1 {element1, element2}
$myHashTable.Entry1
element1
element2
If you have your data in an array you can group the array and convert to a hash table:
ary = $ary + [PSCustomObject]@{RowNumber = 1; EmployeeId = 1; Value = 1 }
ary + [PSCustomObject]@{RowNumber = 2; EmployeeId = 1; Value = 2 }
ary + [PSCustomObject]@{RowNumber = 3; EmployeeId = 2; Value = 3 }
ary + [PSCustomObject]@{RowNumber = 4; EmployeeId = 2; Value = 4 }
ary + [PSCustomObject]@{RowNumber = 5; EmployeeId = 3; Value = 5 }
ary | Group-Object -Property EmployeeId -AsHashTable
$ht is then:
Name Value
---- -----
3 {@{RowNumber=5; EmployeeId=3; Value=5}}
2 {@{RowNumber=3; EmployeeId=2; Value=3}, @{RowNumber=4; EmployeeId=2; Value=4}}
1 {@{RowNumber=1; EmployeeId=1; Value=1}, @{RowNumber=2; EmployeeId=1; Value=2}}
You already got a helpful answer for the first part of your question.
This is my try at the second part - how to assign members of nested hash tables. There isn't an easy built-in syntax to set nested values while creating any not-yet-existing parent hash tables, so I've created a reusable function Set-TreeValue for that purpose.
function Set-TreeValue( $HashTable, [String] $Path, $Value, [String] $PathSeparator = '\.' ) {
# To detect errors like trying to set child of value-type leafs.
Set-StrictMode -Version 3.0
do {
# Split into root key and path remainder (", 2" -> split into max. 2 parts)
$key, $Path = $Path -split $PathSeparator, 2
if( $Path ) {
# We have multiple path components, so we may have to create nested hash table.
if( -not $HashTable.Contains( $key ) ) {
$HashTable[ $key ] = [ordered] @{}
}
# Enter sub tree.
$HashTable = $HashTable[ $key ]
}
else {
# We have arrived at the leaf -> set its value
$HashTable[ $key ] = $Value
}
}
while( $Path )
}
Demo:
$ht = [ordered] @{}
Set-TreeValue $ht foo.bar.baz 42 # Create new value and any non-existing parents
Set-TreeValue $ht foo.bar.baz 8 # Update existing value
Set-TreeValue $ht foo.bar.bam 23 # Add another leaf
Set-TreeValue $ht fop 4 # Set a leaf at root level
#Set-TreeValue $ht fop.zop 16 # Outputs an error, because .fop is a leaf
Set-TreeValue $ht 'foo bar' 15 # Use a path that contains spaces
$ht | ConvertTo-Json -Depth 99 # Output the content of the hash table
Output:
{
"foo": {
"bar": {
"baz": 8,
"bam": 23
}
},
"fop": 4,
"foo bar": 15
}
NOTE: I've opted to create nested hash tables as OrderedDictionary as these are much more useful than regular ones (e. g. to ensure an order in a JSON output). Remove [ordered] if you want unordered hash tables (which propably have slight performance advantage).
Use the index operator to reference a specific entry by key and then assign a new value to that entry:
$hashtable = @{}
# this will add a new key/value entry
$hashtable['abc'] = 1
# this will overwrite the existing value associated with the key `abc`
$hashtable['abc'] = 2
If you have a large code base with many existing calls to .Add($key, $value) and would like to avoid refactoring every call, you can modify the behavior of the hashtable itself so that Add acts like the indexer:
function New-NonStrictHashTable {
return @{} |Add-Member -MemberType ScriptMethod -Name Add -Value {
param($key,$value)
$this[$key] = $value
} -Force -PassThru
}
Now you can do:
# Create hashtable with Add() overriden
$hashtable = New-NonStrictHashTable
$key,$value = 'key','value'
# This works like before
$hashtable.Add($key, $value)
# This works too now, it simply updates the existing entry
$hashtable.Add($key, 'some other value')
This will work for any PowerShell script statement that calls $hashtable.Add() because resolution of ETS methods (like the one we attached to the hashtable with Add-Member) takes precedence over the underlying .NET method.
Another issue with hashes that I have is this:
- Assume you have a hashtabel $motherOfAll which will eventually contain other hashtables, which in turn will also contain hashtables.
- Now you want to insert something into the bottommost layer of hashtables. You first need to check, that all the hashtables along the way exist and contain the proper keys.
- If not, you have to insert a bunch of empty hashtables, which get filled with another empty one... not ad infinitum of course, but still ugly. More messy code. Is there a better way?
The desired behavior you describe here is found in Perl and is known as autovivification:
my %users;
# the nested hashes $users{YeOldHinnerk} and $users{YeOldHinnerk}{contact_details}
# will automatically come into existence when this assignment is evaluated
$users{YeOldHinnerk}{contact_details}{email_address} = "[email protected]"
The Wikipedia article linked above gives an example of how to implement similar behavior in C#, which can be adapted for PowerShell as follows:
Add-Type -TypeDefinition @'
using System.Collections.Generic;
public class AVD
{
private Dictionary<string, object> _data = new Dictionary<string, object>();
public object this[string key]
{
get {
if(!_data.ContainsKey(key)){
_data[key] = new AVD();
}
return _data[key];
}
set {
_data[key] = value;
}
}
}
'@
Now we can take advantage of PowerShell's native index access syntax:
PS ~> $autovivifyingHashtable = [AVD]::new()
PS ~> $autovivifyingHashtable['a']['b']['c'] = 123
PS ~> $autovivifyingHashtable['a'] -is [AVD]
True
PS ~> $autovivifyingHashtable['a']['b'] -is [AVD]
True
PS ~> $autovivifyingHashtable['a']['b']['c']
123
This seems like a bug since the syntax to retrieve the Keys entry is superseding the HashTable's Keys property, though I could see one expecting it to behave either way. According to the Adding a key of 'keys' to a Hashtable breaks access to the .Keys property issue on GitHub, this is a bug but would require a breaking change to correct, so the workaround below was added to the documentation.
According to about_Hash_Tables:
If the key name collides with one of the property names of the HashTable type, you can use
PSBaseto access those properties. For example, if the key name iskeysand you want to return the collection of Keys, use this syntax:
$hashtable.PSBase.Keys
You could also retrieve the property value through reflection...
PS> $ht.GetType().GetProperty('Keys').GetValue($ht)
text1
Keys
$ht | Select-Object -ExpandProperty Keys
I think what you want is something more like this:
$colors = @("black","white","yellow","blue")
$Applications=@{}
Foreach ($i in $colors)
{
$Applications[$i] = @{
Colour = $i
Prod = 'SrvProd05'
QA = 'SrvQA02'
Dev = 'SrvDev12'
}
}
I will also point out that Hashtables often need to be handled defensively. Each key must be unique but values do not need to be. Here is the typical method of handling that:
$colors = @("black","white","yellow","blue")
$Applications=@{}
Foreach ($i in $colors)
{
if($Applications.ContainsKey($i)){
#Do things here if there is already an entry for this key
}else{
$Applications[$i] = @{
Colour = $i
Prod = 'SrvProd05'
QA = 'SrvQA02'
Dev = 'SrvDev12'
}
}
}
EBGreen's helpful answer offers a solution for what you likely meant to do.
To complement that with an explanation of why your code failed:
When you use
+to "add" two hashtables, their entries are merged: in other words: the entries of the RHS are added to the LHS hashtable.
(Technically, a new instance is created with the merged entries.)However - by sensible design - merging is only performed if the hashtables have no keys in common; otherwise, you'll get the error message you saw, complaining about duplicate keys.
If this safeguard weren't in place, you would lose data if the values associated with duplicate entries differ.
Since your loop repeatedly tried to merge a hashtable with the same keys directly into an existing hashtable, your 2nd loop iteration invariably failed.
You can verify this more simply:
$Applications = @{} # create empty hashtable.
# Merge a hashtable literal into $Applications.
# This works fine, because the two hashtables have no keys in common.
$Applications += @{ first = 1; second = 2 }
# $Application now contains the following: @{ first = 1; second = 2 }
# If you now try to add a hashtable with the same set of keys again,
# the operation invariably fails due to duplicate keys.
$Applications += @{ first = 10; second = 20 } # FAILS
# By contrast, adding a hashtable with unique keys works fine:
$Applications += @{ third = 3; fourth = 4 } # OK
# $Application now contains: @{ first = 1; second = 2; third = 3; fourth = 4 }
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 add / remove entries on demand.
- they are lighter-weight than
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 typeSystem.Collections.Specialized.OrderedDictionary, 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 withConvertFrom-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.
- 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
You can overcome this limitation with a custom conversion function,
ConvertFrom-HashTable(source code below); e.g. (inspect the result withFormat-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[[object] $entry.Key] = ConvertFrom-HashTable -HashTable $entry.Value # NOTE: Casting to [object] prevents problems with *numeric* hashtable keys.
} else { # Copy value as-is.
$oht[[object] $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