Adding rows to hash table?

i have a list of FQDNs, i want to create a hash table. in each row the name column is “dns” and the value column is one of the fqdns.

i was previously removing the domain from the fqdn and including both the short name and fqdn in the output. so this works:

foreach($entry in $list)
    {
    $thiscerthost = $entry.fqdn.replace('.domain.com','')
    $thiscertSANs += @{'dns' = $entry.fqdn},@{'dns'= ($thiscerthost)} # include short name
    }

but now i don’t want the short name in it. so i took off the second part of the second line. but now i get errors on every row in the source about the dns key already existing.

foreach($entry in $list)
    {
    $thiscertSANs += @{'dns' = $entry.fqdn} # exclude short name
    }

Item has already been added. Key in dictionary: ‘dns’ Key being added: ‘dns’

i’m sure there’s a better way to do this, but i’m confused why the way i’m doing it worked when i was adding two

@{something},@{somethingelse}

but doesn’t work with only

@{something}.

I’m not sure if I really got what you want to do but I guess using a PSCustomObject might help in this case:

$thiscertSANs =
foreach ($entry in $list) {
    [PSCustomObject]@{
        dns = $entry.fqdn
    }
}
$thiscertSANs

@Olaf’s answer provides a solution as it creates a hash table. But I enjoy these sorts of puzzles as they can provide experience.

@john-curtiss - I believe the error you received is due to hash tables using name/value pairs as the index, unlike arrays which use sequential numbers. You might have duplicates in either your source or transformed data.

My curiosity was also piqued by $entry.fqdn. This indicates that $list is already in some form of collection like [DataTable]. And so, thinking outside the box, I created a 6x2 box for the source data. Then I put a subset of those data into a dictionary object where the key is the fqdn URI. The value in each row is dns which can be described as “type” or some such metadata later.

Subtext: use fqdn as the unique key in a collection. Extract the keys based on the generalized value.

Dictionary versus hash table discussions litter the internet. I like using dictionaries as they are well structured and should beat a hash table in data retrieval when sizes grow large

As for the multidimensional array list, I like MDAs for well-structured data because of the way the data are stored. And, since the indexes are integers, you can get funky with data storage using loops. --When I squint (really squint) at MDAs, I see T-SQL’s hierarchyid and FOR XML EXPLICIT.

<#
Key is unique; use FQDN as URI.

Shape:

key  value
------------
<fqdn> 'dns'
<fqdn> 'dns'
<fqdn> 'dns'
<fqdn> 'dns'
<fqdn> 'dns'

#>
using namespace System.Collections.Generic;
using namespace System.Data;

$list = [string[,]]::new(6,2);  # 6 rows, 2 columns;
    $list[0,0] = 'AutoDiscover';
    $list[0,1] = 'autodiscover.domain.com';
    $list[1,0] = 'ActiveSync';
    $list[1,1] = 'activesync.domain.com';
    $list[2,0] = 'Mail Services';
    $list[2,1] = 'mailservices.domain.com';
    $list[3,0] = 'om-xchangca';
    $list[3,1] = 'om-xchangca.domain.com';
    $list[4,0] = 'owa';
    $list[4,1] = 'owa.domain.com'
    $list[5,0] = '<none>';
    $list[5,1] = 'domain.com';

$thiscertSANs = [Dictionary[string,string]]::new();

for($i = 0;$i -lt $list.GetUpperBound(0); $i++)
{
  $thiscertSANs.Add("$($list[$i,1])",'dns');
}

## Results
cls;
$thiscertSANs;   # or $thiscertSANs.GetEnumerator();
echo "`n`n";
($thiscertSANs.Keys).Where({$thiscertSANs.Values -eq 'dns'});

Results

Results

$list is imported from a csv that has many more columns than just FQDN, and other parts of my script are doing other things to other parts of the csv.
but, for testing, given a csv with two columns called “column” and “FQDN”, where column has anything and FQDN has this.domain.com and that.domain.com.

PS C:\Users\john.curtiss [06/04/2021 07:35:10]> $list

column       fqdn           
------       ----           
anything     this.domain.com
anythingelse that.domain.com

the following code produces a hash table

foreach($entry in $list)
    {
   $thiscerthost = $entry.fqdn.replace('.domain.com','')
   #$thiscertSANs += @{'dns' = $entry.fqdn}  #exclude short name
   $thiscertSANs += @{'dns' = $entry.fqdn},@{'dns'= ($thiscerthost)} # include short name
    }

which looks like this:

PS C:\Users\john.curtiss [06/04/2021 07:39:27]> $thiscertSANs

Name                           Value                                                                                                                                                                                                          
----                           -----                                                                                                                                                                                                          
dns                            this.domain.com                                                                                                                                                                                                
dns                            this                                                                                                                                                                                                           
dns                            that.domain.com                                                                                                                                                                                                
dns                            that                        

i need the same hash table with only the FQDNs. i have tried the following code

foreach($entry in $list)
    {
   #$thiscerthost = $entry.fqdn.replace('.domain.com','')
   $thiscertSANs += @{'dns' = $entry.fqdn}  #exclude short name
   #$thiscertSANs += @{'dns' = $entry.fqdn},@{'dns'= ($thiscerthost)} # include short name
    }

but it gives me “Item has already been added. Key in dictionary: ‘dns’ Key being added: ‘dns’” and produces a hash table that does not contain both FQDNs:

PS C:\Users\john.curtiss [06/04/2021 07:42:29]> $thiscertSANs

Name                           Value                                                                                                                                                                                                          
----                           -----                                                                                                                                                                                                          
dns                            this.domain.com       

PS C:\Users\john.curtiss [06/04/2021 07:43:51]> $thiscertSANs | Get-Member


   TypeName: System.Collections.Hashtable

Have you tried my code suggestion?

@olaf, yes, it produces an object that doesn’t look like my hash table; and get-member says it is something other than a hash table.

$thiscertSANs

dns            
---            
this.domain.com
that.domain.com



PS C:\Users\john.curtiss [06/04/2021 08:18:42]> $thiscertSANs | Get-Member


   TypeName: System.Management.Automation.PSCustomObject

OK … but yours is actually an array of single value hashtables and not a “proper” hashtable. What is it actually what you’re trying to do? Whatfor do you need such a weird hashtable collection?

Just to show what I mean.
Your type of “hashtable”:

$Hashtable = @(
@{dns = 'this.domain'},
@{dns = 'that.domain'}
)
$Hashtable | Get-Member

A proper hashtable:

$ProperHashtable = @{
    dns1 = 'this.domain'
    dns2 = 'that.domain'
}
$ProperHashtable | Get-Member

With the proper hashtable you can access the values by specifying the according key:

$ProperHashtable.dns2

With your way you wouldn’t be able to pick one single element:

$Hashtable.dns

You always get all of them.

i am passing $thiscertsans as a parameter to another command that requires it to be a hashtable. it’s adding the FQDNs as subject alternative names to an SSL certificate it’s creating via an API.

so if i establish $thiscertsans as an array first, this seems to work. i guess i didn’t have to do that before because i was adding two items at a time and it automatgically assumed it was an array. :

$thiscertSANs = @()
foreach($entry in $list)
    {
    $thiscertSANs += @{'dns' = $entry.fqdn}  #exclude short name
     }

Is that something you wrote yourself? Can you show it? Maybe you make it harder that needed.

no, it is not.
https://venafitppps.readthedocs.io/en/latest/functions/New-TppCertificate/#-subjectaltname

reading that, it does say i can use “multiple hashtables,” which i assume is what an array of hashtables is, which is what i assume was happening automagically before when i was adding two “rows” at a time. i was confused because i was getting “TypeName: System.Collections.Hashtable” from get-member.

if i pass your version of $thiscertsans to that venafi command, it says

“New-TppCertificate : Cannot process argument transformation on parameter ‘SubjectAltName’. Cannot convert value “@{dns=this.domain.com}” to type “System.Collections.Hashtable”. Error: “Cannot convert the “@{dns=this.domain.com}” value of
type “System.Management.Automation.PSCustomObject” to type “System.Collections.Hashtable”.””

@john-curtiss - Good news! I replicated your error before coming up with this approach!

The SubjectAltName parameter might introduce a few more programmatic issues because of its attributes, mainly Accept pipeline input. Therefore, you could attack this by generating a string that you’d paste into the cmdlet.

<#
Desired
-SubjectAltName @{'Email'='me@x.com'},@{'IPAddress'='1.2.3.4'}
#>
using namespace System.Collections;

## Create csv source data
$list = Import-Csv A:\sourceData.csv -Header "column","fqdn";

## Create and populate a collection object to store dynamic strings
$thiscertSANs = [ArrayList]::new();
foreach($entry in $list)
{
    [void]$thiscertSANs.Add( $("'{0}'='{1}'" -f $entry.column,$entry.fqdn));
}

## Results
($thiscertSANs|%{'@{'+$_+'}'}) -join","

A string of hash tables is constructed to be pasted into the SubjectAltName parameter.

Because this is an ArrayList, you can call an instance with a simple index value (this functionality can be magicized). For example, '@{'+$thiscertSANs[-1]+'}' will generate a hashy-string for the last instance.

Results03

Notes

  • The Add() method for the ArrayList does not allow for the hash table declaration in the sting, and must be added later.
  • Using [Dictionary[int,hashtable]] raises the error (or similiar) in the original post. –which is odd. Maybe I fat-fingered somewhere.

Source Data

SourceData

Hmmm … ok at least you could make your way a little easier to read I think:

$List = @'
"fqdn","bla"
"this.domain.com","blu"
"that.domain.com","ble"
"some.domain.com","blo"
'@ | 
ConvertFrom-Csv -Delimiter ','

$thiscertSANs =
foreach ($entry in $List) {
    @{dns = $entry.fqdn}
}
2 Likes

now. how do i remove duplicate rows from $thiscertSANs? get-unique and sort -unique are not doing it for me. they are only returning a single result.

Easy. You do it before you turn them into an array of hashtables … :wink:

$List = @'
"fqdn","bla"
"this.domain.com","blu"
"that.domain.com","ble"
"some.domain.com","blo"
"that.domain.com","bli"
"this.domain.com","blö"
"some.domain.com","blü"
'@ | 
ConvertFrom-Csv -Delimiter ',' |
Sort-Object -Property 'fqdn' -Unique

$thiscertSANs =
foreach ($entry in $List) {
    @{dns = $entry.fqdn }
}

:wink:

$thiscertSANs | sort {$_.values} -Unique
$thiscertSANs | select {$_.values} -unique

these seem to work.

Cool. Now you have 3 options to choose from. :+1:t4: :wink:

This script might add to the work @john-curtiss will perform in providing the automation which includes the New-TppCertificate cmdlet.

@john-curtiss, amend my second post as such:

using namespace System.Collections;
$list = Import-Csv A:\sourceData.csv -Header "column","fqdn"|Select-Object -Unique column,fqdn;

$thiscertSANs = [ArrayList]::new();
foreach($entry in $list)
{
    [void]$thiscertSANs.Add( $("'{0}'='{1}'" -f $entry.column,$entry.fqdn));
}

$output = ($thiscertSANs|%{'@{'+$_+'}'}) -join",";

This provides you with a string variable, output, of hashed data (includes the required dns label for the cmdlet) that you can attempt to pass as the SubjectAltName parameter value. Piping the variable is not likely to work because of the param attributes, but try scripting $output as the parameter value directly in the cmdlet. I do not know if it will be interpreted as a series of hash tables, but I am banking on the format in the cmdlet’s Example 3 and

You can provide more than 1 of the same SAN type with multiple hashtables.

I added duplicate entries into the source data and included a Select…Unique command. Notice that there is not a header row in the content–I tend to supply my own headers for the data during Import-Csv. The source data file now contains:
sourceData

this one is doing what i need:

$thiscertSANs = @()
foreach($entry in $list)
    {
    $thiscertSANs += @{'dns' = $entry.fqdn}  #exclude short name
     }

i’m able to include $thiscertsans in new-tppcertificate this way. thanks very much, guys.

Again … .the slightly better way of collecting elements in an array would be this:

$thiscertSANs = 
foreach ($entry in $list) {
    @{'dns' = $entry.fqdn } 
}

Because PowerShell actually does not have a method for adding elements to an array it’s destroying the original array and creates a new one with the same name and with the elements of the original one plus the one you asked it to add with “+=”. That takes more memory and and more time than simply assigning the output of elements to a variable. :wink: … and - at leat for me - I think it’s a little easier to read.

1 Like