Powershell PSCustomObject Extracting values - powershell

When creating and working with PSCustomObjects which result in a NoteProperty member with a 'Definition' (as shown below), is there any easy, programmatic way to select the values from the definition fields without resorting to splitting the strings?
For example below, is there a 'good' way to extract the value 'silver' from the field of name 'token' that does not requre traditional string manipulations? I've been messing around with select and -ExpandProperty but getting no-where fast and would appreciate a nudge in the right direction.
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
bsw NoteProperty decimal bsw=3.14
name NoteProperty string name=chris
token NoteProperty string token=silver
volume NoteProperty decimal volume=17.22
Thanks.
Update: Following guidance from Thomas I came up with this function to extract Noteproperty members from a PSObject and return a Hashtable with the names of the fields and the values:
function convertObjectToHash($psObj) {
$hashBack = #{}
try {
$psObjFieldNames = $psObj | get-member -type NoteProperty | select "Name"
$psObjFieldNames | foreach-object {
$hashBack.Add($_.Name,$psObj.$($_.Name)) }
}catch{ "Error: $_" }
return $hashBack
}
Thanks!

You can access the members of a custom object like in any other object:
$myCustomObject.token
Reproduction:
$myCustomObject = New-Object -TypeName psobject
$myCustomObject | Add-Member -MemberType NoteProperty -Name bsw -Value 3.14
$myCustomObject | Add-Member -MemberType NoteProperty -Name name -Value "chris"
$myCustomObject | Add-Member -MemberType NoteProperty -Name token -Value "silver"
$myCustomObject | Add-Member -MemberType NoteProperty -Name volume -Value 17.22
$myCustomObject | Get-Member -MemberType NoteProperty
$myCustomObject.token
Output:
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
bsw NoteProperty System.Double bsw=3.14
name NoteProperty string name=chris
token NoteProperty string token=silver
volume NoteProperty System.Double volume=17.22
silver

Since in your objects you store a string value, I see no other way to extract just the value silver then to use a string method to get only the part after the equals sign.
($obj.token -split '=', 2)[-1] --> silver
Why not create the custom objects with an extra property called value and add the value you seek in there, taken from the Definition property? like
$obj = [PsCustomObject]#{'token' = 'string token=silver'; 'value' = 'silver'}

Related

Powershell Add-Members Dynamically

Long story short - it seems impossible to get a pipeline value to resolve to plain text in a ScriptProperty and so I'm looking for a way to either do that, or to somehow parameterize when I'm adding properties to an object. I would be perfectly happy to add the values as NotePropertyMembers rather than a ScriptProperty; but I can't find a way to do that without looping through the entire object multiple times. The advantage of the ScriptProperty is the ability to use the $this variable so that I don't need to call Add-Member on each member of the object itself.
Here's what an example of what I'm trying to accomplish and where it is breaking down.
# This sample file contains the values that I'm interested in
#'
ID,CATEGORY,AVERAGE,MEDIAN,MIN,MAX
100,"",52,50,10,100
100,1,40,40,20,60
100,2,41,35,15,85
'# > values.csv
# To access the values, I decided to create a HashTable
$map = #{}
(Import-Csv values.csv).ForEach({$map[$_.ID + $_.CATEGORY] = $_})
# This is my original object. In reality, it has several additional columns of data - but it is
# missing the above values which is why I want to pull them in
#'
ID,CATEGORY
100,""
100,1
100,2
'# > object.csv
$object = Import-Csv object.csv
# This is the meat of my attempt. Loop through all of the properties in the HashTable that
# I need to pull into my object and add them
$map.Values | Get-Member -MemberType NoteProperty | Where-Object {$_.Name -NotIn 'ID', 'CATEGORY'} | ForEach-Object {$object | Add-Member -MemberType ScriptProperty -Name "$($_.Name)" -Value {$map[$this.ID + $this.CATEGORY]."$($_.Name)"}}
In theory, I know that it works because I can display the values for a particular member of my object using the code below
$map.Values | Get-Member -MemberType NoteProperty | Where-Object {$_.Name -NotIn 'ID', 'CATEGORY'} | ForEach-Object {"$($_.Name) = " + $map[$object[0].ID + $object[0].CATEGORY]."$($_.Name)"}
Displays
AVERAGE = 52
MAX = 100
MEDIAN = 50
MIN = 10
However - it is clear that $_.Name and "$($_.Name)" do not resolve to their plain text description when they're in the ScriptProperty
PS> $object | GM -MemberType ScriptProperty
# Displays
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
AVERAGE ScriptProperty System.Object AVERAGE {get=$map[$this.ID + $this.CATEGORY]."$($_.Name)";}
MAX ScriptProperty System.Object MAX {get=$map[$this.ID + $this.CATEGORY]."$($_.Name)";}
MEDIAN ScriptProperty System.Object MEDIAN {get=$map[$this.ID + $this.CATEGORY]."$($_.Name)";}
MIN ScriptProperty System.Object MIN {get=$map[$this.ID + $this.CATEGORY]."$($_.Name)";}
To me, an obvious workaround is to individually add each column which still allows me to take advantage of using Add-Member on the object itself instead of each individual member. However, the entire idea is to be able to dynamically add values without knowing ahead of time what their names are - in order to do that, I need to find a way to force the name to resolve within the ScriptProperty
# Workaround for this incredibly simple example
PS> $object | Add-Member -MemberType ScriptProperty -Name AVERAGE -Value {$map[$this.ID + $this.CATEGORY]."AVERAGE"}
PS> $object | Add-Member -MemberType ScriptProperty -Name MEDIAN -Value {$map[$this.ID + $this.CATEGORY]."MEDIAN"}
PS> $object | Add-Member -MemberType ScriptProperty -Name MIN -Value {$map[$this.ID + $this.CATEGORY]."MIN"}
PS> $object | Add-Member -MemberType ScriptProperty -Name MAX -Value {$map[$this.ID + $this.CATEGORY]."MAX"}
PS> $object | GM -MemberType ScriptProperty
# Displays
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
AVERAGE ScriptProperty System.Object AVERAGE {get=$map[$this.ID + $this.CATEGORY]."AVERAGE";}
MAX ScriptProperty System.Object MAX {get=$map[$this.ID + $this.CATEGORY]."MAX";}
MEDIAN ScriptProperty System.Object MEDIAN {get=$map[$this.ID + $this.CATEGORY]."MEDIAN";}
MIN ScriptProperty System.Object MIN {get=$map[$this.ID + $this.CATEGORY]."MIN";}
Complementing the helpful answer from Mathias which creates new objects, here is how you can approach dynamically updating the object itself:
$exclude = 'ID', 'CATEGORY'
$map = #{}
$properties = $values[0].PSObject.Properties.Name | Where-Object { $_ -notin $exclude }
$values.ForEach{ $map[$_.ID + $_.CATEGORY] = $_ | Select-Object $properties }
foreach($line in $object) {
$newValues = $map[$line.ID + $line.CATEGORY]
foreach($property in $properties) {
$line.PSObject.Properties.Add(
[psnoteproperty]::new($property, $newValues.$property)
)
}
}
Above code assumes that both Csvs ($values and $object) are loaded in memory and that you know which properties from the $values Csv should be excluded.
Use Select-Object instead of Add-Member:
$object |Select *,#{Name='Average';Expression={$map[$_.ID + $_.Category].Average}},#{Name='Median';Expression={$map[$_.ID + $_.Category].Median}},#{Name='Min';Expression={$map[$_.ID + $_.Category].Min}},#{Name='Max';Expression={$map[$_.ID + $_.Category].Max}}
Select-Object will evaluate the Expression block against every single individual input item, thereby resolving the mapping immediately, as opposed to deferring it until someone references the property
One additional option that I stumbled upon while working with Santiago's great solution. Using Add-Member rather than the Properties.Add() method would allow for a hash table of NotePropertyMembers to used, which could be advantageous although I haven't started testing performance of different methods yet. It makes things a bit simpler if you already have a hash table of the properties and values that you want to add to your object.
$map = #{}
# This will create a hash table nested in the hash table, rather than a hash table whose values are PSCustomObjects
$values.ForEach{$map[$_.ID + $_.CATEGORY] = $_.PSObject.Properties | ?{$_.Name -notin $exclude} | % {$ht = #{}} {$ht[$_.Name] = $_.Value} {$ht}}
ForEach($line in $object){
If($map.Contains($line.ID + $line.CATEGORY){
$line | Add-Member -NotePropertyMembers $map[$line.ID + $line.CATEGORY]
}
}
Santiago's answer and your own, based on static NoteProperty members rather than the originally requested dynamic ScriptProperty members, turned out to be the better solution in the case at hand.
As for how you could have made your dynamic ScriptProperty approach work:
$map.Values |
Get-Member -MemberType NoteProperty |
Where-Object Name -NotIn ID, CATEGORY |
ForEach-Object {
# Cache the property name at hand, so it can be referred to
# in the pipeline below.
$propName = $_.Name
# .GetNewClosure() is necessary for capturing the value of $amp
# as well as the current value of $propName as part of the script block.
$object |
Add-Member -MemberType ScriptProperty `
-Name $propName `
-Value { $map[$this.ID + $this.CATEGORY].$propName }.GetNewClosure()
}
Note that use of .GetNewClosure() is crucial, so as to capture:
each iteration's then-current $propName variable value
the $map variable's hashtable
as part of the script block.
Note:
.GetNewClosure() is expensive, as it creates a dynamic module behind the scenes in which the captured values are cached and to which the script block gets bound.
Generally, it's better to restrict ScriptProperty members to calculations that are self-contained, i.e. rely only on the state of the object itself (accessible via automatic $this variable), not also on outside values; a simple example:
$o = [pscustomobject] #{ A = 1; B = 2}
$o | Add-Member -Type ScriptProperty -Name C { $this.A + $this.B }
$o.C # -> 3
$o.A = 2; $o.C # -> 4

Powershell How to extract hashtable value from nested hashtable

I've made an API request to the ManageEngine tech support database which returns a JSON response which is returned as a custom object of three hash tables: $result.response_status, $result.list_info, and $result.requests The third of these contains the data I'm looking for.
$results.requests.id an integer
$results.requests.subject a string
$results.requester a hashtable.
The requester hash table is #requester{name= 'Martin Zimmerman'; email='mzimmer#company.com'}
What I'd like to be able to do is:
$results.requests | select id, subject, requester.name
to get a single line displaying the id, subject and requester's name
id subject name
-- -------------- --------------
3329 Can't open file Martin Zimmerman
However, I cannot figure out the nomenclature to extract the value of the name key in the requester hash table.
I think this is what you want. You need to build a Calculated Property (Example 10).
$results.requests | Select-Object id,subject,#{l='name';e={$_.requester['name']}}
This approach allows you to call the name key from the hashtable.
EDIT
If your requester item is a PSCustomObject, then try this.
$results.requests | Select-Object id,subject,#{l='name';e={$_.requester.name}}
Test with similar object structure.
$results = New-Object psobject
$requests = New-Object psobject
$requests | Add-Member -MemberType NoteProperty -Name id -Value 2
$requests | Add-Member -MemberType NoteProperty -Name subject -Value "lol"
$hash = #{}
$hash.Add("email","mzimmerman#company.com")
$hash.Add("name","Martin Zimmerman")
$requests | Add-Member -MemberType NoteProperty -Name requester -Value $hash
$results | Add-Member -MemberType NoteProperty -Name requests -Value $requests
$results.requests | select id,subject,#{l='name';e={$_.requester['name']}}
id subject name
-- ------- ----
2 lol Martin Zimmerman

Correct way to add a string property to existing Powershell object?

So for example, I have a custom object for $server. I want to create a new property, $server.output to contain multi line output of varying size. What is the best approach to take? There doesn't seem to be any straight forward information on the web around how to add simple string properties to objects?
FYI I am using powershell v4, but it would be nice to have a solution for v5 too.
Outside of re-creating the object or ensuring the property exists on object initialization, you have to use Add-Member to create new properties on a pscustomobject:
# alternatively, use `-Force` on `Add-Member`
if (-not ($obj | Get-Member -Name output))
{
$obj | Add-Member -MemberType NoteProperty -Name output -Value ''
}
Supported in v3+:
$obj | Add-Member -NotePropertyName output -NotePropertyValue '' -Force
Add multiple members:
$obj | Add-Member -NotePropertyMembers #{output = ''; output1 = ''} -Force
Add-Member

Dealing with (members of) heterogeneous arrays

Let's create books
$a = New-Object –TypeName PSObject
$a | Add-Member –MemberType NoteProperty –Name Title –Value "Journey to the West"
$a | Add-Member –MemberType NoteProperty –Name Price –Value 12
$b = New-Object –TypeName PSObject
$b | Add-Member –MemberType NoteProperty –Name Title –Value "Faust"
$b | Add-Member –MemberType NoteProperty –Name Author –Value "Goethe"
$array1 = $a,$b
$array2 = $b,$a
Now let's display these two arrays
PS D:\Developpement\Powershell> $array1
Title Price
----- -----
Journey to the West 12
Faust
PS D:\Developpement\Powershell> $array2
Title Author
----- ------
Faust Goethe
Journey to the West
So as far as I understand this basically means that what powershell consider to be properties of an array are the properties of its first element (in fact that's not even true because if the first element is $null the next one will be considered). Now that also implies that :
if you call Get-Member on the array, you will only get members of the first element
if you call Convert-ToCvs on the array, you will only export property values for properties defined by the first element
etc
I hardly understand the rationals behind that and this behaviour has made it infuriatingly painful for me to work with heterogeneous arrays in powershell.
I'd like to import data from various external sources, process them and then export them to a cvs file. Items are similar but most of them miss some properties unpredictably. Is there any obvious way to handle that in Powershell without reprogramming the wheel?
This is the way it has to be because PowerShell uses pipelines. When you run ex. $array1 | Export-CSV ...., PowerShell starts to write to the CSV-file as soon as the first object arrives. At that point it needs to know what the header will look like as that is the first line in a csv-file. So PowerShell has to assume that the class/properties of the first object represents all the remaining objects in the pipeline. The same goes for Format-Table and similar commands that need to set a style/view before outputting any objects.
The usual workaround to this is to specify the header manually using Select-Object. It will add all missing properties to all objects with a value of $null. This way, all the objects sent to ex. Export-CSV will have all the same properties defined.
To get the header, you need to receive all unique property-names from all objects in your array. Ex.
$array1 |
ForEach-Object { $_.PSObject.Properties} |
Select-Object -ExpandProperty Name -Unique
Title
Price
Author
Then you can specify that as the header using Select-Object -Properties Title,Price,Author before sending the objects to Export-CSV Ex:
$a = New-Object –TypeName PSObject
$a | Add-Member –MemberType NoteProperty –Name Title –Value "Journey to the West"
$a | Add-Member –MemberType NoteProperty –Name Price –Value 12
$b = New-Object –TypeName PSObject
$b | Add-Member –MemberType NoteProperty –Name Title –Value "Faust"
$b | Add-Member –MemberType NoteProperty –Name Author –Value "Goethe"
$array = $a,$b
$AllProperties = $array |
ForEach-Object { $_.PSObject.Properties} |
Select-Object -ExpandProperty Name -Unique
$array | Select-Object -Property $AllProperties | Export-CSV -Path "mycsv.out" -NoTypeInformation
This will create this CSV-file:
"Title","Price","Author"
"Journey to the West","12",
"Faust",,"Goethe"
If you have mulltiple arrays you can combine them like this $array = $array1 + $array2

How to add a Name NoteProperty to an object?

How can add Name NoteProperty for an object? I tried:
$a = "This", "Is", "a", "cat"
$a | Add-Member -type NoteProperty -name Name
$a
but this doesn't seem to work.
The expected output is:
Name
----
This
Is
a
cat
This is the answer to the amended question:
$a = "This", "Is", "a", "cat"
$a | Select-Object #{Name='Name'; Expression={$_}}
Output, as requested, is
Name
----
This
Is
a
cat
Here is an example of how to take your example each value in $a, convert it to a PSObject with a Name and Value properties as well as using the Add-Member cmdlet. The ` is for line continuation. Because the Add-Member is being called in a pipeline, the -passThru property was used to pass the object with the new member on.
$a | %{ new-object psobject -property #{Name="String"; Value=$_}} `
| %{ Add-Member -inputObject $_ -passThru -type NoteProperty -name Note -Value Value}
I piped the output to | ft -auto to shrink the columns to fit here nicely.
Value Name Note
----- ---- ----
This String Value
Is String Value
a String Value
cat String Value
Another way of answering the updated question:
$a | %{new-object psobject -p #{Name=$_}
Expected output matches:
Name
----
This
Is
a
cat