I'm creating an array of PSObjects with calculated properties. I need one property that is calculated based on another property of the same object. How do I do that?
Example - let's say I have array of strings like "a_1", "b_2", "c_3" etc. and I have a lookup function that returns something based on the first part of those strings, i.e. someLookUpFunction('a') would return "AA" with input of "a".
Now I need a property in my object that has this calculated 'AA' based on the my 'name' property
$stringArray = #('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | ForEach-Object{
New-Object PSObject -Property #{
'name' = ($_ -split "_")[0]
'extendedName' = {$name = ($_ -split "_")[0]; someLookUpFunction($name) }
}
}
The code above doesn't work in part that the output for 'extendedName' property is just this script block. How do I make it to take the value?
If you need to capture the output of an expression within an expression, you can use the sub-expression operator $().
$stringArray = #('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | ForEach-Object {
[pscustomobject]#{
'name' = ($_ -split "_")[0]
# You can't reference the name property above in this property because it has not been created yet.
'extendedName' = $($name = ($_ -split "_")[0]; someLookUpFunction $name)
}
}
However, that should not be necessary in your example. You can define a variable before the custom object creation and then reference it within the object creation code:
$stringArray = #('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | ForEach-Object {
$name = ($_ -split '_')[0]
[pscustomobject]#{
'name' = $name
'extendedName' = someLookUpFunction $name
}
}
You could also pass expressions to parameters directly provided it can be tokenized correctly:
$stringArray = #('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | ForEach-Object {
[pscustomobject]#{
'name' = ($_ -split '_')[0]
'extendedName' = someLookUpFunction ($_ -split '_')[0]
}
}
Note: The proper way to call a function without using the pipeline is functionName -parametername parametervalue or functionName parametervalue if positional parameters are enabled. The syntax functionName(parametervalue) could have unintended consequences. See this answer for a deeper dive into function/method calling syntax.
You cannot access the name property of an object before that object has been created.
In addition to AdminOfThings Good Answer you can bypass the loop altogether using a select statement with the calculated property hash syntax:
$stringArray = #('a_1', 'b_2', 'c_3')
$objectArray = $stringArray |
Select-Object #{Name = 'Name'; Expression = { ($_ -Split '_')[0] } },
#{Name = 'ExtendedName'; Expression = { SomeLookupFunction ($_ -Split '_')[0] } }
For the efficiency of not executing -Split '_' 2x, if you do go with a loop just use a variable to and reference twice.
Altered version of AdminOfThings Example:
$stringArray = #('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | ForEach-Object {
$TmpName = ($_ -split '_')[0]
[pscustomobject]#{
'name' = $TmpName
'extendedName' = someLookUpFunction $TmpName
}
}
It's also correct that you can't reference a property before it's been added to an object. One way around this is to just use 2 select statements:
$stringArray = #('a_1', 'b_2', 'c_3')
$objectArray = $stringArray |
Select-Object #{Name = 'Name'; Expression = { ($_ -Split '_')[0] } } |
Select-Object *, #{Name = 'ExtendedName'; Expression = { SomeLookupFunction ($_ -Split '_')[0] } }
This may have some readability advantage, but, I try to avoid it in favor of invoking as few commands as possible.
Related
I've got a txt file with the following content:
#test.txt
'ALDHT21;MIMO;1111;BOK;Tree'
'ALDHT21;MIMO;1211;BOK;Tree'
'PRGHT21;AIMO;1351;STE;Water'
'PRGHT21;AIMO;8888;FRA;Stone'
'ABCDT22;DIDO;8888;STE;Stone'
'PRA2HT21;ADDO;8888;STE;Stone'
';ADDO;1317;STE;Stone'
To make it easier to explain, let's give the above content headers:
''Group;Code;ID;Signature;Type'
With the help of Powershell, I'm trying to create a foreach loop of each unique "Signature" to return two variables with unique data from rows where the "Signature" exists in and then mashed together with some delimiters.
Based on the file content, here are the expected results:
First loop:
$Signature = "BOK"
$Groups = "Tree:ALDHT21"
$Codes = "Tree:MIMO"
Next loop:
$Signature = "FRA"
$Groups = "Stone:PRGHT21"
$Codes = "Stone:AIMO"
Last loop:
$Signature = "STE"
$Groups = "Stone:PRA2HT21,Stone:ABCDT22,Water:PRGHT21"
$Codes = "Stone:ADDO,Stone:DIDO,Water:AIMO"
Notice the last loop should skip the last entry in the file because it contains an empty Group.
My attempt didn't quite hit the mark and I'm struggling to find a good way to accomplish this:
$file = "C:\temp\test.txt"
$uniqueSigs = (gc $file) -replace "'$|^'" | ConvertFrom-Csv -Delimiter ';' -Header Group,Code,ID,Signature,Type | group Signature
foreach ($sigs in $uniqueSigs) {
$Groups = ""
foreach ($Group in $sigs.Group) {
$Groups += "$($Group.Type):$($Group.Group),"
}
$Groups = $Groups -replace ",$"
[PSCustomObject] #{
Signatur = $sigs.Name
Groups = $Groups
}
$Codes = ""
foreach ($Group in $sigs.Group) {
$Codes += "$($Group.Type):$($Group.Code),"
}
$Codes = $Codes -replace ",$"
[PSCustomObject] #{
Signatur = $sigs.Name
Codes = $Codes
}
$Signature = $sigs.Name
If ($Group.Group){
write-host "$Signature "-" $Groups "-" $Codes "
}
}
Result from my bad attempt:
BOK - Tree:ALDHT21,Tree:ALDHT21 - Tree:MIMO,Tree:MIMO
FRA - Stone:PRGHT21 - Stone:AIMO
Any help appreciated. :)
Your variables are somewhat confusingly named; the following streamlined solution uses fewer variables and perhaps produces the desired result:
$file = "test.txt"
(Get-Content $file) -replace "'$|^'" | ConvertFrom-Csv -Delimiter ';' -Header Group,Code,ID,Signature,Type |
Group-Object Signature |
ForEach-Object {
# Create and output an object with group information.
# Skip empty .Group properties among the group's member objects.
# Get the concatenation of all .Group and .Code column
# values each, skipping empty groups and eliminating duplicates.
$groups = (
$_.Group.ForEach({ if ($_.Group) { "$($_.Type):$($_.Group)" } }) |
Select-Object -Unique
) -join ","
$codes = (
$_.Group.ForEach({ "$($_.Type):$($_.Code)" }) |
Select-Object -Unique
) -join ","
# Create and output an object comprising the signature
# and the concatenated groups and codes.
[PSCustomObject] #{
Signature = $_.Name
Groups = $groups
Codes = $codes
}
# Note: This is just *for-display* output.
# Don't use Write-Host to output *data*.
Write-Host ($_.Name, $groups, $codes -join ' - ')
}
Output:
BOK - Tree:ALDHT21 - Tree:MIMO
FRA - Stone:PRGHT21 - Stone:AIMO
STE - Water:PRGHT21,Stone:ABCDT22,Stone:PRA2HT21 - Water:AIMO,Stone:DIDO,Stone:ADDO
Signature Groups Codes
--------- ------ -----
BOK Tree:ALDHT21 Tree:MIMO
FRA Stone:PRGHT21 Stone:AIMO
STE Water:PRGHT21,Stone:ABCDT22,Stone:PRA2HT21 Water:AIMO,Stone:DIDO,Stone:ADDO
Note that the for-display Write-Host surprisingly precedes the the default output formatting for the [pscustomobject] instances, which is due to the asynchronous behavior of the implicitly applied Format-Table formatting explained in this answer.
I have few [pscustomobject] objects that can have not all properties.
For example:
PS> $1 = [pscustomobject]#{ A='a1'; B='b1' }
PS> $2 = [pscustomobject]#{ A='a2'; C='c2' }
And I try to display all properties with Format-Table like this:
PS> $1,$2 | Format-Table
A B
- -
a1 b1
a2
PS> $2,$1 | Format-Table
A C
- -
a2 c2
a1
But every time it displays only properties from first object in collection.
I want to display all properties like if I set -Property argument explicitly.
PS> $1,$2 | Format-Table -Property A,B,C
A B C
- - -
a1 b1
a2 c2
Setting -Property argument is good if:
All set of properties is known in advance
Collection is small and I can get all properties with Get-Member -MemberType Properties
But I have a huge collection (above 10000 objects) with unknown properties so I need help with it.
REMARK: Format-Table will be used only for small slices (10-100 elements).
For that, you can use below function to merge all properties into the first object:
function Complete-ObjectHeaders {
# function to add properties to the first item in a collection of PSObjects
# when this object is missing properties from items further down the array.
# you may need this if you have such a collection and want to export it
# to Csv, since Export-Csv (and also Format-Table) only looks at the FIRST
# item to create the csv column headers.
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0)]
[PSObject[]]$Collection,
[int]$MaxItemsToTest = -1, # < 0 --> test all items in the collection
[switch]$SortHeaders
)
# Try and find all headers by looping over the items in the collection.
# The headers will be captured in the order in which they are found.
if ($MaxItemsToTest -gt 0) {
$MaxItemsToTest = [math]::Min($MaxItemsToTest, $Collection.Count)
$headers = for($i = 0; $i -lt $MaxItemsToTest; $i++) {
($Collection[$i].PSObject.Properties).Name
}
$headers = $headers | Select-Object -Unique
}
else {
$headers = $Collection | ForEach-Object {($_.PSObject.Properties).Name} | Select-Object -Unique
}
if ($SortHeaders) { $headers = $headers | Sort-Object }
# update the first object in the collection to contain all headers
$Collection[0] = $Collection[0] | Select-Object $headers
,$Collection
}
Use like this:
$1 = [pscustomobject]#{ A='a1'; B='b1' }
$2 = [pscustomobject]#{ A='a2'; C='c2' }
# just output to console
Complete-ObjectHeaders -Collection $1,$2 | Format-Table -AutoSize
# or capture the merged array of objects in a new variable you can save as CSV file for instance
$merged = Complete-ObjectHeaders -Collection $1,$2
$merged | Export-Csv -Path 'D:\Test\Merged.csv' -NoTypeInformation
Output:
A B C
- - -
a1 b1
a2 c2
Thanks #theo for the answer.
I used it to write my own version of a function that supports pipelining.
function Expand-Properties {
[Cmdletbinding()]
param(
[Parameter(ValueFromPipeline)]
$InputObject,
[Parameter()]
[Alias('All')]
[switch]
$ExpandAll,
[Parameter()]
[switch]
$SortHeaders
)
begin {
$collection = [System.Collections.ArrayList]::new()
$properties = [System.Collections.ArrayList]::new()
}
process {
[void]$collection.Add($InputObject)
$properties.AddRange((($InputObject.PSObject.Properties).Name))
}
end {
if ($SortHeaders) {
$properties = $properties | Sort-Object -Unique
} else {
$properties = $properties | Select-Object -Unique
}
if ($ExpandAll) {
for ($i = 0; $i -lt $collection.Count; ++$i) {
$collection[$i] = $collection[$i] | Select-Object -Property $properties
}
} else {
$collection[0] = $collection[0] | Select-Object -Property $properties
}
$collection
}
}
EXAMPLE:
PS> $1 = [pscustomobject]#{ A='a1'; B='b1' }
PS> $2 = [pscustomobject]#{ A='a2'; C='c2' }
PS> $1, $2 | Expand-Properties
A B C
- - -
a1 b1
a2 c2
I would like to make a new line in my hashtable to extract it in a csv.
I initialize my variable in hastable
$vlr=#{}
$vlr["OS"]=,#("test","test2")
I extract my variable in a .csv
$Output += New-Object PSObject -Property $vlr
$output | Convert-OutputForCSV | export-csv -NoTypeInformation -Delimiter ";" -Path $filepath
and the problem is in the extraction the result of the values is on the same line
My goal is that each value is in a different line
You might want to use the Out-String cmdlet for this:
$vlr=#{}
$vlr["OS"]=,#("test","test2") | Out-String
$Object = New-Object PSObject -Property $vlr
$Object | ConvertTo-Csv
"OS"
"test
test2
"
this solution does not work because in the case where $vlr with several names the extraction will be complicated
$vlr=#{}
$vlr["OS"]=,#("test","test2")
$vlr["PS"]=,#("lous","tique")
it's a problem
https://gallery.technet.microsoft.com/scriptcenter/Convert-OutoutForCSV-6e552fc6
For the function Convert-OutputForCSV
I don't know what the posted function does, but you can make your own function to handle a single-key or multi-key hash table provided all of the key value counts are the same.
function Convert-OutputForCsv {
param(
[parameter(ValueFromPipeline)]
[hashtable]$hash
)
# Array of custom object property names
$keys = [array]$hash.Keys
# Loop through each key's values
for ($i = 0; $i -lt $hash.($keys[0]).count; $i++) {
# Custom object with keys as properties. Property values are empty.
$obj = "" | Select $keys
# Loop through key names
for ($j = 0; $j -lt $keys.Count; $j++) {
$obj.($keys[$j]) = $hash.($Keys[$j])[$i]
}
$obj
}
}
$vlr=[ordered]#{}
$vlr["OS"]='test','test2'
$vlr["PS"]='lous','tique'
$vlr | Convert-OutputForCsv | Export-Csv -NoTypeInformation -Delimiter ";" -Path $filepath
Honestly, if you are in control of the input data, I would just type out a CSV instead of typing out hash tables.
this solution is good in my simplified case but not adapted to my case unfortunately
I'm merging my old base2 array with my new base array and my goal is to concatenate the values in an excel to make them usable
$base2 = Get-content $filepath2 | select -first 1
$base2 = $base2 -split ";"
$base2 = $base2.Replace("`"", "")
$cunt2 = $base2.count - 1
$h2 = ipcsv $filepath2 -Delimiter ";"
$HashTable2 = #{}
for ($i = 0 ; $i -le $cunt2 ; $i++) {
foreach ($r in $h2) {
$HashTable2[$base2[$i]] = $r.($base2[$i])
}
base2 = old tables
$base = Get-content $filepath2 | select -first 1
$base = $base -split ";"
$base = $base.Replace("`"", "")
$cunt = $base.count - 1
$h1 = ipcsv $filepath -Delimiter ";"
$HashTable = #{}
for ($i = 0 ; $i -le $cunt ; $i++) {
foreach ($r in $h1) {
$HashTable[$base[$i]] = $r.($base[$i])
}
New tables $base
once the two arrays are initialized, I merge them and this is where I have to separate the values row by row
$csvfinal = $hashtable, $hashtable2 | Merge-Hashtables
Let's say I have an object like this:
$test = #{
ThisIsTheFirstColumn = "ValueInFirstColumn";
ThisIsTheSecondColumn = "ValueInSecondColumn"
}
and I want to end up with:
$test = #{
THIS_IS_THE_FIRST_COLUMN = "ValueInFirstColumn";
THIS_IS_THE_SECOND_COLUMN = "ValueInSecondColumn"
}
without manually coding the new column names.
This shows me the values I want:
$test.PsObject.Properties | where-object { $_.Name -eq "Keys" } | select -expand value | foreach{ ($_.substring(0,1).toupper() + $_.substring(1) -creplace '[^\p{Ll}\s]', '_$&').Trim("_").ToUpper()} | Out-Host
which results in:
THIS_IS_THE_FIRST_COLUMN
THIS_IS_THE_SECOND_COLUMN
but now I can't seem to figure out how to assign these new values back to the object.
You can modify hashtable $test in place as follows:
foreach($key in #($test.Keys)) { # !! #(...) is required - see below.
$value = $test[$key] # save value
$test.Remove($key) # remove old entry
# Recreate the entry with the transformed name.
$test[($key -creplace '(?<!^)\p{Lu}', '_$&').ToUpper()] = $value
}
#($test.Keys) creates an array from the existing hashtable keys; #(...) ensures that the key collection is copied to a static array, because using the .Keys property directly in a loop that modifies the same hashtable would break.
The loop body saves the value for the input key at hand and then removes the entry under its old name.[1]
The entry is then recreated under its new key name using the desired name transformation:
$key -creplace '(?<!^)\p{Lu} matches every uppercase letter (\p{Lu}) in a given key, except at the start of the string ((?<!^)), and replaces it with _ followed by that letter (_$&); converting the result to uppercase (.ToUpper()) yields the desired name.
[1] Removing the old entry before adding the renamed one avoids problems with single-word names such as Simplest, whose transformed name, SIMPLEST, is considered the same name due to the case-insensitivity of hasthables in PowerShell. Thus, assigning a value to entry SIMPLEST while entry Simplest still exists actually targets the existing entry, and the subsequent $test.Remove($key) would then simply remove that entry, without having added a new one.
Tip of the hat to JosefZ for pointing out the problem.
I wonder if it is possible to do it in place on the original object?
($test.PsObject.Properties|Where-Object {$_.Name -eq "Keys"}).IsSettable says False. Hence, you need do it in two steps as follows:
$test = #{
ThisIsTheFirstColumn = "ValueInFirstColumn";
ThisIsTheSecondColumn = "ValueInSecondColumn"
}
$auxarr = $test.PsObject.Properties |
Where-Object { $_.Name -eq "Keys" } |
select -ExpandProperty value
$auxarr | ForEach-Object {
$aux = ($_.substring(0,1).toupper() +
$_.substring(1) -creplace '[^\p{Ll}\s]', '_$&').Trim("_").ToUpper()
$test.ADD( $aux, $test.$_)
$test.Remove( $_)
}
$test
Two-step approach is necessary as an attempt to perform REMOVE and ADD methods in the only pipeline leads to the following error:
select : Collection was modified; enumeration operation may not execute.
Edit. Unfortunately, the above solution would fail in case of an one-word Pascal Case key, e.g. for Simplest = "ValueInSimplest". Here's the improved script:
$test = #{
ThisIsTheFirstColumn = "ValueInFirstColumn";
ThisIsTheSecondColumn = "ValueInSecondColumn"
Simplest = "ValueInSimplest" # the simplest (one word) PascalCase
}
$auxarr = $test.PsObject.Properties |
Where-Object { $_.Name -eq "Keys" } |
select -ExpandProperty value
$auxarr | ForEach-Object {
$aux = ($_.substring(0,1).toupper() +
$_.substring(1) -creplace '[^\p{Ll}\s]', '_$&').Trim("_").ToUpper()
$newvalue = $test.$_
$test.Remove( $_)
$test.Add( $aux, $newvalue)
}
$test
This seems to work. I ended up putting stuff in a new hashtable, though.
$test = #{
ThisIsTheFirstColumn = "ValueInFirstColumn";
ThisIsTheSecondColumn = "ValueInSecondColumn"
}
$test2=#{}
$test.PsObject.Properties |
where-object { $_.Name -eq "Keys" } |
select -expand value | foreach{ $originalPropertyName=$_
$prop=($_.substring(0,1).toupper() + $_.substring(1) -creplace '[^\p{Ll}\s]', '_$&').Trim("_").ToUpper()
$test2.Add($prop,$test[$originalPropertyName])
}
$test2
I'm pretty new to Powershell and have a problem.
It seems I can get neither the Compare-Object nor the Sort-Object funtions to work.
I got two Arraylists which I fill with 10 objects of type "Table".
#Create Arraylists
$list1 = new-object System.Collections.ArrayList
$list2 = new-object System.Collections.ArrayList
#Declare Table-Class
class Table {
[String] $name
[Int] $number
}
#Fill list1
[Int] $i = 0
while ($i -lt 10) {
$entry = new-Object Table
$entry.name = "Name";
$entry.number = $i;
$list1.Add($entry)
$i++
}
#Fill list2
[Int] $j = 10
while ($j -gt 0) {
$entry = new-Object Table
$entry.name = "name";
$entry.number = $j;
$list2.Add($entry)
$j--
}
Now I want to compare these two ArrayLists like this:
Compare-Object $list1 $list2 | ForEach-Object {$_.InputObject}
This doesn't seem to work and I think it's because I'm not really doing anything with the comparison. If someone could help me with the correct Syntax I'd be really happy.
Anyway, I think this comparison would somehow return a $false boolean.
If that were true, I'd like to sort $list2 by $entry.number.
I'm attempting this like that:
$list2 = $list2 | Sort-Object -Property { $_[1] }
But my list doesn't change at all. I already tried several different "solutions" but it seems none of these is a real "solution".
I'd be really glad if someone could help me or at least point me in the right direction.
EDIT: For future readers, here is the working syntax:
#sort list by the property number
$list2 = $list2 | sort-object -Property number
#count differences and if differences greater than 0, do stuff
[Int] $diff
$properties = $list1 | Get-Member -MemberType Property | Select-Object -ExpandProperty Name
foreach ($property in $properties) {
$diff = 0
$diff = (Compare-Object $list1 $list2 -Property "$property").count
if ($diff -ne 0) {
#do stuff
}
else {
#do something else
}
}
Your sorting does not work because -Property expects you to pass the name of an actual property. In your case the class two properties: $name and $number. You can also check what properties an object has by using $list2 | Get-Member. So in order to sort, you can use:
$list2 = $list2 | Sort-Object -Property number
Now the reason Compare-Object is not working is because it's implemented differently that one might expect. See this answer explaining how it works.
One other thing you should keep in mind: Calling $list2.Add($entry) actually returns an integer (the index of the inserted element). PowerShell returns all uncaptured output. This can cause some unexpected behavior/output if you are not careful. So you should get in the habit of writing [void] $list2.Add($entry), unless you really want to return those indexes.