Hashtable - Combining unknown number of hashtables - powershell

I'm trying to programmatically combine an unknown number of hashtables into a larger hashtable. Each individual table will have the same keys. I tried just appending it, but it throws an error about duplicate keys.
ForEach ($Thing in $Things){
$ht1 = New-Object psobject #{
Path = $Thing.path
Name = $Thing.name
}
$ht2 += $ht1
}
That throws the error
Item has already been added. Key in dictionary: 'Path' Key being added: 'Path'
The end result would be that later I can say
ForEach ($Item in $ht2){
write-host $Item.path
write-host $Item.name
}

Converting my comment to an answer.
What you probably want to create is an array of hashtables. Each item in the array can have its own value for each key. This structure can be used in the way you indicate in your query at the end of your post.
Try this:
$things = gci $home
$ht2 = #() # create empty array
ForEach ($Thing in $Things){
$ht1 = New-Object psobject #{
Path = $Thing.PSpath
Name = $Thing.name
}
$ht2 += $ht1
}
$ht2
Note that I changed .path to .PSpath in order to make the example work. Note that $ht2 gets initialized to an empty array before looping.

Adding an element to a hash table indeed fails if there already is an element with same key. To overcome that, first see if the element exists. Like so,
if(-not $ht2.ContainsKey($thing.name)){
$ht2.Add($thing.name, $thing.value)
}
It is also possible to use item property [] for the check, since it returns $null for non-existing element. Like so,
if($ht2[$thing.name] -eq $null){
$ht2.Add($thing.name, $thing.value)
}

If this really is about merging Hashtables as the title says, then basically you have two options to add the entries from the second hashtable into the first.
1. Use the static method Add() (first item 'wins')
This has already been explained in vonPryz's answer, but in short:
Adding an entry in a hashtable with a key that already exists in the hash, the Add() method will throw an exception, because all keys in a hash must be unique.
To overcome that, you need to check if an entry with that key exists and if so, do not add the entry from the second hash.
foreach ($key in $secondHash.Keys) {
if (!$firstHash.Contains($key)) {
# only add the entry if the key (name) did not already exist
$firstHash.Add($key, $secondHash[$key])
}
}
This way, all entries already in the first hashtable will NOT get overwritten and duplicate entries from the second hash are discarded.
2. Overwriting/adding regardless of the existance (last item 'wins')
You can also opt to 'merge' the entries without the need for checking like this:
foreach ($key in $secondHash.Keys) {
# always add the entry even if the key (name) already exist
$firstHash[$key] = $secondHash[$key]
}
This way, if an entry already existed in the first hash, its value will be overwritten with the value from the second hash.
If the entry did not already exist, it is simply added to the first hashtable.
But, what if you want to merge without skipping or overwriting existing values?
In that case, you need to come up with some method of creating a unique key for the entry to add.
Something like this:
foreach ($key in $secondHash.Keys) {
if ($firstHash.Contains($key)) {
# ouch, the key already exists..
# now, we only want to add this if the value differs (no point otherwise)
if ($firstHash[$key] -ne $secondHash[$key]) {
# add the entry, but create a new unique key to store it under first
# this example just adds a number between brackets to the key
$num = 1
do {
$newKey = '{0}({1})' -f $key, $num++
} until (!$firstHash.Contains($newKey))
# we now have a unique new key, so add it
$firstHash[$newKey] = $secondHash[$key]
}
}
else {
# no worries, the key is unique
$firstHash[$key] = $secondHash[$key]
}
}

Turns out what I needed for my results was an array of hashtables, not a hashtable of hashtables, as pointed out by #WalterMitty. My final code was:
#varibale name ht2 kept for clarity in how it relates to original question
$ht2 = #()
ForEach ($Thing in $Things){
$ht1 = New-Object psobject #{
Path = $Thing.path
Name = $Thing.name
}
$ht2 += $ht1
}

Related

Check if a condition is met by a line within a TXT but "in an advanced way"

I have a TXT file with 1300 megabytes (huge thing). I want to build code that does two things:
Every line contains a unique ID at the beginning. I want to check for all lines with the same unique ID if the conditions is met for that "group" of IDs. (This answers me: For how many lines with the unique ID X have all conditions been met)
If the script is finished I want to remove all lines from the TXT where the condition was met (see 2). So I can rerun the script with another condition set to "narrow down" the whole document.
After few cycles I finally have a set of conditions that applies to all lines in the document.
It seems that my current approach is very slow.( one cycle needs hours). My final result is a set of conditions that apply to all lines of code.
If you find an easier way to do that, feel free to recommend.
Help is welcome :)
Code so far (does not fullfill everything from 1&2)
foreach ($item in $liste)
{
# Check Conditions
if ( ($item -like "*XXX*") -and ($item -like "*YYY*") -and ($item -notlike "*ZZZ*")) {
# Add a line to a document to see which lines match condition
Add-Content "C:\Desktop\it_seems_to_match.txt" "$item"
# Retrieve the unique ID from the line and feed array.
$array += $item.Split("/")[1]
# Remove the line from final document
$liste = $liste -replace $item, ""
}
}
# Pipe the "new cleaned" list somewhere
$liste | Set-Content -Path "C:\NewListToWorkWith.txt"
# Show me the counts
$array | group | % { $h = #{} } { $h[$_.Name] = $_.Count } { $h } | Out-File "C:\Desktop\count.txt"
Demo Lines:
images/STRINGA/2XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg images/STRINGA/3XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg images/STRINGB/4XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg images/STRINGB/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg images/STRINGC/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
performance considerations:
Add-Content "C:\Desktop\it_seems_to_match.txt" "$item"
try to avoid wrapping cmdlet pipelines
See also: Mastering the (steppable) pipeline
$array += $item.Split("/")[1]
Try to avoid using the increase assignment operator (+=) to create a collection
See also: Why should I avoid using the increase assignment operator (+=) to create a collection
$liste = $liste -replace $item, ""
This is a very expensive operation considering that you are reassigning (copying) a long list ($liste) with each iteration.
Besides it is a bad practice to change an array that you are currently iterating.
$array | group | ...
Group-Object is a rather slow cmdlet, you better collect (or count) the items on-the-fly (where you do $array += $item.Split("/")[1]) using a hashtable, something like:
$Name = $item.Split("/")[1]
if (!$HashTable.Contains($Name)) { $HashTable[$Name] = [Collections.Generic.List[String]]::new() }
$HashTable[$Name].Add($Item)
To minimize memory usage it may be better to read one line at a time and check if it already exists. Below code I used StringReader and you can replace with StreamReader for reading from a file. I'm checking if the entire string exists, but you may want to split the line. Notice I have duplicaes in the input but not in the dictionary. See code below :
$rows= #"
images/STRINGA/2XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGA/3XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGB/4XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGB/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGC/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGA/2XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGA/3XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGB/4XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGB/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
images/STRINGC/5XXXXXXXX_rTTTTw_GGGG1_Top_MMM1_YY02_ZZZ30_AAAA5.jpg
"#
$dict = [System.Collections.Generic.Dictionary[int, System.Collections.Generic.List[string]]]::new();
$reader = [System.IO.StringReader]::new($rows)
while(($row = $reader.ReadLine()) -ne $null)
{
$hash = $row.GetHashCode()
if($dict.ContainsKey($hash))
{
#check if list contains the string
if($dict[$hash].Contains($row))
{
#string is a duplicate
}
else
{
#add string to dictionary value if it is not in list
$list = $dict[$hash].Value
$list.Add($row)
}
}
else
{
#add new hash value to dictionary
$list = [System.Collections.Generic.List[string]]::new();
$list.Add($row)
$dict.Add($hash, $list)
}
}
$dict

Casting Object to String Array Powershell

I want to create an array of strings instead of a variable object so that I can use the "contains" keyword on each index of the array.
$myArray = Get-ADDomain
The above creates an object, which is not what I want. I also tried
[string[]] $myArray = Get-ADDomain
But after that, $myArray only contains one string and it is the first non-empty property of Get-ADDomain, in my case "ComputersContainer". What should I do to receive an array of strings where each string is a different property, such as
$myArray[0] = "AllowedDNSSuffixes = {}"
PowerShell will always return objects by design of course, and specifying that [string[]], does not really change that.
For what you are trying to use, you have to force the array creation. The below is just one way, but I am sure others will have more elegant ways of doing this as well. Though I am curious why one would want to do this, this way. But, hey, that's just me.
# Create an empty array
$DomainData = #()
# Get all the data points for the utilized cmdlet, split on a common delimiter for the array
[string[]]$DomainData = (Get-ADDomain | Select *) -split ';'
# Display the array count
$DomainData.Count
34
# validate getting a value from the array by using an index number
$Item = $DomainData[17]
NetBIOSName=CONTOSO
[array]::IndexOf($DomainData, $Item)
17
# Use that element number to validate the use of the contains comparison operator
0..($DomainData.Count - 1) | %{ If($DomainData[$_] -contains $item){"Index key is $_ contains a value of $Item"} }
Index key is 17 contains a value of NetBIOSName=CONTOSO
# Use the previous with a partial string for a comparison, -contains cannot be used, like or match has to be used
# From the documentation:
# -Contains
# Description: Containment operator. Tells whether a collection of reference values includes a single test value.
$Item = '*domain*'
0..($DomainData.Count - 1) | %{ If($DomainData[$_] -like $item){"Index key is $_ like a value of $Item"} }
Index key is 1 like a value of *domain*
Index key is 6 like a value of *domain*
Index key is 7 like a value of *domain*
Index key is 8 like a value of *domain*
Index key is 18 like a value of *domain*
Index key is 20 like a value of *domain*
You cannot cast a PSObject directly to a string array like that.
However, this can be accomplished rather easily.
To get an array of string from the object
$myArray = Get-ADDomain
# You can use a standard array #() but these tends to be slower for bigger amount of data
$outArray = New-Object -TypeName System.Collections.Generic.List[String]
#To add just the value
$myArray.psobject.properties | Foreach { $outArray.Add($_.Value) }
# To add Name = {Value} instead
$myArray.psobject.properties | Foreach { $outArray.Add("$($_.Name) = {$($_.Value)}") }
Using an hasthable instead:
$myArray = Get-ADDomain
$hashtable = #{}
$myArray.psobject.properties | Foreach { $hashtable[$_.Name] = $_.Value }
# If you need to do something with the key
Foreach ($key in $hashtable.Keys) {
$Value = $hashtable[$key]
if ($value -like '*prod*') {
Write-Host $key
}
}

Unable to remove item from hash table

In Powershell, I have a hash table that contains data similar to this -
Name Value
---- -----
1-update.bat 1
2-update.bat 2
3-update.bat 3
3.1-update.bat 3.1
4-update.bat 4
I also have an variable that contians a number, for example 3
What I would like to do is loop through the array and remove any entry where the value is less than or equal to 3
I'm thinking that this will be easy, especially as the docs say that has tables contain a .remove method. However, the code I have below fails, yeilding this error -
Exception calling "Remove" with "1" argument(s): "Collection was of a
fixed size."
Here is the code that I used -
$versions = #{}
$updateFiles | ForEach-Object {
$versions.Add($_.name, [decimal]($_.name -split '-')[0])
}
[decimal]$lastUpdate = Get-Content $directory\$updatesFile
$versions | ForEach-Object {
if ( $_.Value -le $lastUpdate ) {
$versions.Remove($version.Name)
}
}
I first tried to loop $versions in a different manner, trying both the foreach and for approaches, but both failed in the same manner.
I also tried to create a temporary array to hold the name of the versions to remove, and then looping that array to remove them, but that also failed.
Next I hit Google, and while I can find several similar questions, none that answer my specific question. Mostly they suggest using a list (New-Object System.Collections.Generic.List[System.Object]), whcih from what I can tell is of no help to me here.
Is anyone able to suggest a fix?
Here you go, you can use .Remove(), you just need a clone of the hashtable so that it will let you remove items as you enumerate.
[hashtable]$ht = #{ '1-update.bat'=1;'2-update.bat'=2;'3-update.bat'=3;'3.1-update.bat'=3.1; '4-update.bat'=4 }
#Clone so that we can remove items as we're enumerating
$ht2 = $ht.Clone()
foreach($k in $ht.GetEnumerator()){
if([decimal]$k.Value -le 3){
#notice, deleting from clone, then return clone at the end
$ht2.Remove($k.Key)
}
}
$ht2
Notice I've cast the original variable too so that it's explicitly a hash table, may not be required, but I like to do it to at least keep things clear.
It looks like you just confused ForEach-Object with foreach but only halfway (maybe it was foreach before and you converted it).
You can't send a [hashtable] directly to ForEach-Object; the $_ in that case will just refer to the single [hashtable] you sent in. You can do:
foreach ($version in $versions.GetEnumerator()) {
$version.Name
$version.Value
}
or you can do something like this:
$versions.Keys | ForEach-Object {
$_ # the name/key
$versions[$_] # the value
$versions.$_ # also the value
}
$ht.Keys # list all keys
$ht[$_] # take an element of hastable
$ht.Remove($_) # remove an element of hastable by his key
what you want:
$ht.Keys | ? { $ht[$_] -le 3 } | %{$ht.Remove($_) }
You need to create a temporary array to hold the name/key of the versions to remove, and then looping that array to remove them from hash table:
$versionKeysToRemove = $versions.Keys | Where-Object { $versions[$_] -le $lastUpdate }
$versionKeysToRemove | ForEach-Object { $versions.Remove($_) }
Or shorter:
($versions.Keys | ? { $versions[$_] -le $lastUpdate }) | % { $versions.Remove($_) }
Please note the parentheses.

How do I retrieve a HashTable key from a value?

I have a PowerShell HashTable that contains a set of key-value pairs (naturally). All of the HashTable values are unique.
I would like to retrieve a HashTable key, based on a value that I specify, using PowerShell.
Another options:
to iterate over the HashTable keys and find a key that contains the
value:
$HashTable.Keys |? { $HashTable[$_] -eq $Val }
to iterate using GetEnumerator() function:
$HashTable.GetEnumerator() | ?{ $_.Value -eq $Val } | %{ $_.Key }
You can use PowerShell 4.0's Where method syntax to achieve this. The Where method accepts a PowerShell ScriptBlock to find objects matching the specified criteria. We can iterate over the HashTable keys and find a key that contains the desired value.
In case you do have a scenario where you have duplicate HashTable values, you can optionally specify a second parameter, of type WhereOperatorSelectionMode, that specifies which objects should be returned by the call to the Where method. By specifying First for the second method parameter, we can ensure that only a single HashTable key is ever returned.
All of the supported values for the second parameter are as follows:
Default
First
Last
SkipUntil
Until
Split
$HashTable = #{
1 = 10;
2 = 20;
3 = 30;
}
$Val = 30;
$HashTable.Keys.Where({ $HashTable[$PSItem] -eq $Val; }, [System.Management.Automation.WhereOperatorSelectionMode]::First);

PowerShell: How can I traverse through registry values named "SQLArg1", "SQLArg2" etc.?

I am working on a PowerShell function that is supposed to check if two values in the registry named "SQLArg4" and "SQLArg5" (or two other numbers) are set to a certain value content and set them if not.
The problem is I cannot enumerate the values to traverse through them to compare all of them and then add my two value contents if they are not present.
I tried creating a string out of "SQLArg" and a $i index but PowerShell would not allow me to use that string as a field of a variable.
Any ideas?
It sounds like you only need to work with one registry key (no recursion). Try this code. It will get all the values under $key that start with "SQLArgs" and store them in $values. It loops through a number sequence and tests for the existence of the key values named SQLArgs#. When it finds one it will set the key value data.
$key = "HKCU:\Andy"
$values = Get-ItemProperty -Path $key -Name SQLArg*
1..20 | % {
if ($values."SQLArg$_" -ne $null) {
Set-ItemProperty -Path $key -Name "SQLArg$_" -Value "Powershell Rocks"
}
}