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";}
CodePudding user response:
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.
CodePudding user response:
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
CodePudding user response:
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]
}
}
CodePudding user response:
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