Hashtable Parameter Acting Like Reference Variable - powershell

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

Related

How do I convert a powershell hashtable to an object?

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

Generated string names in Calculated Properties aren't accepted by select cmdlet

I want to generate the following table:
AAA BBB CCC
--- --- ---
10 10 10
10 10 10
10 10 10
10 10 10
10 10 10
So I write the following code using a foreach loop to generate the column names:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = $_; expression = { 10 } }
}
#(1..5) | select -Property $property
But I get the following error saying the name is not a string:
select : The "name" key has a type, System.Management.Automation.PSObject, that is not valid; expected type is System.String.
At line:4 char:11
+ #(1..5) | select -Property $property
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Select-Object], NotSupportedException
+ FullyQualifiedErrorId : DictionaryKeyIllegalValue2,Microsoft.PowerShell.Commands.SelectObjectCommand
To get the code work, I have to convert the $_ to string like below:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = [string]$_; expression = { 10 } }
}
#(1..5) | select -Property $property
Or like below:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = $_; expression = { 10 } }
}
$property | foreach { $_.name = [string]$_.name }
#(1..5) | select -Property $property
The question is: the $_ is already a string. Why do I have to convert it to string again? And why select thinks that the name is PSObject?
To confirm that it's already a string, I write the following code to print the type of name:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = $_; expression = { 10 } }
}
$property | foreach { $_.name.GetType() }
The following result confirms that it's already a string:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
True True String System.Object
True True String System.Object
I know that there are many other easier ways to generate the table. But I want to understand why I have to convert a string to string to make the code work, and why select doesn't think that the string is a string. For what it's worth, my $PSVersionTable.PSVersion is:
Major Minor Build Revision
----- ----- ----- --------
5 1 18362 1474
You're seeing the unfortunate effects of incidental, normally invisible [psobject] wrappers PowerShell uses behind the scenes.
In your case, because the input strings are supplied via the pipeline, they get wrapped in and stored as [psobject] instances in your hashtables, which is the cause of the problem.
The workaround - which is neither obvious nor should it be necessary - is to discard the wrapper by accessing .psobject.BaseObject:
$property = 'AAA', 'BBB', 'CCC' | ForEach-Object {
#{ name = $_.psobject.BaseObject; expression = { 10 } }
}
1..5 | select -Property $property
Note:
In your case, a simpler alternative to .psobject.BaseObject (see the conceptual about_Intrinsic Members help topic) would have been to call .ToString(), given that you want a string.
To test a given value / variable for the presence of such a wrapper, use -is [psobject]; with your original code, the following yields $true, for instance:
$property[0].name -is [psobject]
Note, however, that this test is meaningless for [pscustomobject] instances, where it is always $true (custom objects are in essence [psobject] instances without a .NET base objects - they only have dynamic properties).
That the normally invisible [psobject] wrappers situationally, obscurely result in behavioral differences is arguably a bug and the subject of GitHub issue #5579.
Simpler and faster alternative, using the .ForEach() array method:
$property = ('AAA', 'BBB', 'CCC').ForEach({
#{ name = $_; expression = { 10 } }
})
1..5 | select -Property $property
Unlike the pipeline, the .ForEach() method does not wrap $_ in [psobject], so the problem doesn't arise and no workaround is needed.
Using the method is also faster, although note that, unlike the pipeline, it must collect all its input in memory up front (clearly not a problem with an array literal).

Don't quite understand powershells "-Join" and "ConvertTo-Csv"

I have this code to create a csv and display it as a table:
$Keys = ("OrderDate","Region","Rep","Product","Units","Unit Cost","Total")
$Csv = #()
$Keys | ForEach-Object {
$Csv += $_ -Join ","
}
$Csv | ConvertFrom-Csv
My output is:
OrderDate
OrderDate,Region
OrderDate,Region,Rep
OrderDate,Region,Rep,Product
OrderDate,Region,Rep,Product,Units
OrderDate,Region,Rep,Product,Units,Unit Cost
OrderDate,Region,Rep,Product,Units,Unit Cost,Total
OrderDate
---------
Region
Rep
Product
Units
Unit Cost
Total
I would like these lines to be formatted as headers of a table, like this:
OrderDate Region Rep ProductUnits Unit Cost Total
----------- -------- ---- --------------- ----------- ------
What you're attempting can be done like this:
$Keys = "OrderDate","Region","Rep","Product","Units","Unit Cost","Total"
$Object = Select-Object -InputObject 0 -Property $Keys
That's typically most useful when you don't know what the properties are when writing the script. The input object doesn't matter as long as it's a scalar value (i.e., not a collection).
To get the exact output you've specified, you'd have to do $Object | Format-Table because PowerShell defaults to list output for custom objects, but that changes the object into a string so it's just for display.
A more generally useful pattern to create an object when you do know the properties is to do this:
$Object = [PSCustomObject]#{
"OrderDate" = # Some Value
"Region" = # Some Value
"Rep" = # Some Value
"Product" = # Some Value
"Units" = # Some Value
"Unit Cost" = # Some Value
"Total" = # Some Value
}
Here you instance the object and assign the values you want immediately.

Powershell: Unable to search imported hashtable

I'm new to Powershell, and this behavior has me stumped.
Let's say I've created a hashtable with the following information:
$hash= #{
California = 'Sacramento';
Washington = 'Olympia';
Oregon = 'Salem';
Alaska = 'Juneau';
}
$hash
Now I want to search for a particular key using a where -match statement:
$result = #($layers.getEnumerator() | ? {$_.key -match 'Cal'})
$result
This returns with California for the key and Sacramento for the value. All is working as it should.
Now, instead of typing in the keys and values for the table, let's say I want to import them into the hashtable using a CSV file:
California,Sacramento
Washington,Olympia
Oregon,Salem
Alaska,Junea
Here is my code I created to import the CSV, which works fine:
Import-Csv C:\Users\hgordon\Desktop\Maps_to_search\junk.csv -Header "Key","Value"
$layers = #{}
Foreach ($key in $keys) {
$layers[$key] = $csv
}
My problem is that when I run my search statement above, it returns nothing.
Why won't the search work on the imported hashtable? Is there something that I need to modify with the import statement? What is the difference between an imported hashtable vs one that is manually typed?
Thanks!
Edit: I've edited my import script as follows to make it clearer:
$t = Import-Csv -Path C:\Users\hgordon\Desktop\Maps_to_search\junk.csv -Header "Key","Value"
$HashTable = #{}
foreach($r in $t)
{
#Write-Host $r.column1 $r.column2
$HashTable[$r.Key] = $r.Value
}
$HashTable
I still get no result from my search script, though.
To get the job done, you need just to get the output of Import-Csv and use it this way:
$layers = Import-Csv "C:\yourfile.csv" -Header "Key","Value"
$result = #($layers | ? {$_.Key -match 'Cal'})
$result
You don't need to create a HashTable.
So in your current iteration, Import-Csv is importing the CSV as a PSCustomObject type. Your Key/Value headers are being used to create a property on the object with an array under each of the CSV, so you end up with:
key value
--- -----
California Sacramento
Washington Olympia
Oregon Salem
Alaska Junea
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
key NoteProperty string key=California
value NoteProperty string value=Sacramento
To build your own hashtable from this information, you'd need to merge it yourself:
$HT = #{}
$Csv = Import-Csv ... -Header 'Keys','Values'
For ($i = 0; $i -lt $Csv.Keys.Count; $i++)
{
## Here we're using array-accessors
$HT[$Csv.Keys[$i]] = $Csv.Values[$i]
#alternative
#$HT.($Csv.Keys[$i]) = $Csv.Values[$i]
}

Looping through a hash, or using an array in PowerShell

I'm using this (simplified) chunk of code to extract a set of tables from SQL Server with BCP.
$OutputDirectory = 'c:\junk\'
$ServerOption = "-SServerName"
$TargetDatabase = "Content.dbo."
$ExtractTables = #(
"Page"
, "ChecklistItemCategory"
, "ChecklistItem"
)
for ($i=0; $i -le $ExtractTables.Length – 1; $i++) {
$InputFullTableName = "$TargetDatabase$($ExtractTables[$i])"
$OutputFullFileName = "$OutputDirectory$($ExtractTables[$i])"
bcp $InputFullTableName out $OutputFullFileName -T -c $ServerOption
}
It works great, but now some of the tables need to be extracted via views, and some don't. So I need a data structure something like this:
"Page" "vExtractPage"
, "ChecklistItemCategory" "ChecklistItemCategory"
, "ChecklistItem" "vExtractChecklistItem"
I was looking at hashes, but I'm not finding anything on how to loop through a hash. What would be the right thing to do here? Perhaps just use an array, but with both values, separated by space?
Or am I missing something obvious?
Shorthand is not preferred for scripts; it is less readable. The %{} operator is considered shorthand. Here's how it should be done in a script for readability and reusability:
Variable Setup
PS> $hash = #{
a = 1
b = 2
c = 3
}
PS> $hash
Name Value
---- -----
c 3
b 2
a 1
Option 1: GetEnumerator()
Note: personal preference; syntax is easier to read
The GetEnumerator() method would be done as shown:
foreach ($h in $hash.GetEnumerator()) {
Write-Host "$($h.Name): $($h.Value)"
}
Output:
c: 3
b: 2
a: 1
Option 2: Keys
The Keys method would be done as shown:
foreach ($h in $hash.Keys) {
Write-Host "${h}: $($hash.$h)"
}
Output:
c: 3
b: 2
a: 1
Additional information
Be careful sorting your hashtable...
Sort-Object may change it to an array:
PS> $hash.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Hashtable System.Object
PS> $hash = $hash.GetEnumerator() | Sort-Object Name
PS> $hash.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
This and other PowerShell looping are available on my blog.
Christian's answer works well and shows how you can loop through each hash table item using the GetEnumerator method. You can also loop through using the keys property. Here is an example how:
$hash = #{
a = 1
b = 2
c = 3
}
$hash.Keys | % { "key = $_ , value = " + $hash.Item($_) }
Output:
key = c , value = 3
key = a , value = 1
key = b , value = 2
You can also do this without a variable
#{
'foo' = 222
'bar' = 333
'baz' = 444
'qux' = 555
} | % getEnumerator | % {
$_.key
$_.value
}
I prefer this variant on the enumerator method with a pipeline, because you don't have to refer to the hash table in the foreach (tested in PowerShell 5):
$hash = #{
'a' = 3
'b' = 2
'c' = 1
}
$hash.getEnumerator() | foreach {
Write-Host ("Key = " + $_.key + " and Value = " + $_.value);
}
Output:
Key = c and Value = 1
Key = b and Value = 2
Key = a and Value = 3
Now, this has not been deliberately sorted on value, the enumerator simply returns the objects in reverse order.
But since this is a pipeline, I now can sort the objects received from the enumerator on value:
$hash.getEnumerator() | sort-object -Property value -Desc | foreach {
Write-Host ("Key = " + $_.key + " and Value = " + $_.value);
}
Output:
Key = a and Value = 3
Key = b and Value = 2
Key = c and Value = 1
Here is another quick way, just using the key as an index into the hash table to get the value:
$hash = #{
'a' = 1;
'b' = 2;
'c' = 3
};
foreach($key in $hash.keys) {
Write-Host ("Key = " + $key + " and Value = " + $hash[$key]);
}
About looping through a hash:
$Q = #{"ONE"="1";"TWO"="2";"THREE"="3"}
$Q.GETENUMERATOR() | % { $_.VALUE }
1
3
2
$Q.GETENUMERATOR() | % { $_.key }
ONE
THREE
TWO
A short traverse could be given too using the sub-expression operator $( ), which returns the result of one or more statements.
$hash = #{ a = 1; b = 2; c = 3}
forEach($y in $hash.Keys){
Write-Host "$y -> $($hash[$y])"
}
Result:
a -> 1
b -> 2
c -> 3
If you're using PowerShell v3, you can use JSON instead of a hashtable, and convert it to an object with Convert-FromJson:
#'
[
{
FileName = "Page";
ObjectName = "vExtractPage";
},
{
ObjectName = "ChecklistItemCategory";
},
{
ObjectName = "ChecklistItem";
},
]
'# |
Convert-FromJson |
ForEach-Object {
$InputFullTableName = '{0}{1}' -f $TargetDatabase,$_.ObjectName
# In strict mode, you can't reference a property that doesn't exist,
#so check if it has an explicit filename firest.
$outputFileName = $_.ObjectName
if( $_ | Get-Member FileName )
{
$outputFileName = $_.FileName
}
$OutputFullFileName = Join-Path $OutputDirectory $outputFileName
bcp $InputFullTableName out $OutputFullFileName -T -c $ServerOption
}