Dealing with (members of) heterogeneous arrays - powershell

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

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

Group-Object AsHashTable doesn't work for ScriptProperty

Works:
$Names = 1..5 | % { new-object psobject | add-member -Type NoteProperty -Name Name -Value "MyName" -PassThru } | group Name -AsHashTable
$Names.MyName
Doesn't work:
$Names = 1..5 | % { new-object psobject | add-member -Type ScriptProperty -Name Name -Value {"MyName"} -PassThru } | group Name -AsHashTable
$Names.MyName
The reason you're unable to access the values in the hash-table by prop name or key-based access is that the keys/props are wrapped in PSObjects. There was a Github issue to fix this in Powershell Core, but it will likely remain forever in Windows Powershell.
If you want to convert to a hash-table after grouping, and want to access some of the grouped values by property name or key-based access do this:
$Names = 1..5 | ForEach-Object {
New-Object PsObject | Add-Member -Type ScriptProperty -Name Name -Value { return "MyName"} -PassThru
} | Group-Object -Property 'Name' -AsHashTable -AsString
$Names.MyName
$Names['MyName']
If you want to convert to a hash-table after grouping, and want to access all the grouped values at once, do this:
$Names = 1..5 | ForEach-Object {
New-Object PsObject | Add-Member -Type ScriptProperty -Name Name -Value { return "MyName"} -PassThru
} | Group-Object -Property 'Name' -AsHashTable
$Names.Values
If you're not converting to a hash-table after the grouping, and want to access the data in $Names.Group, you'll need to expand that property.
$Names = 1..5 | % {
new-object psobject | add-member -Type ScriptProperty -Name Name -Value {"MyName"} -PassThru
} | Group-Object -Property 'Name'
$Names | Select-Object -ExpandProperty Group

POWERSHELL extract email and use it [duplicate]

In PowerShell, how do you get an object's property value by specifying its name (a string)? I want something like the following:
$obj = get-something
# View the object's members:
$obj | gm
# I could retrieve a property by doing so:
write-host $obj.SomeProp
# But for many purposes, I would really want to:
write-host $obj | Get-PropertyByName "SomeProp"
Is there something similar to "Get-PropertyByName" in PowerShell?
Sure
write-host ($obj | Select -ExpandProperty "SomeProp")
Or for that matter:
$obj."SomeProp"
Expanding upon #aquinas:
Get-something | select -ExpandProperty PropertyName
or
Get-something | select -expand PropertyName
or
Get-something | select -exp PropertyName
I made these suggestions for those that might just be looking for a single-line command to obtain some piece of information and wanted to include a real-world example.
In managing Office 365 via PowerShell, here was an example I used to obtain all of the users/groups that had been added to the "BookInPolicy" list:
Get-CalendarProcessing conferenceroom#example.com | Select -expand BookInPolicy
Just using "Select BookInPolicy" was cutting off several members, so thank you for this information!
You can get a property by name using the Select-Object cmdlet and specifying the property name(s) that you're interested in. Note that this doesn't simply return the raw value for that property; instead you get something that still behaves like an object.
[PS]> $property = (Get-Process)[0] | Select-Object -Property Name
[PS]> $property
Name
----
armsvc
[PS]> $property.GetType().FullName
System.Management.Automation.PSCustomObject
In order to use the value for that property, you will still need to identify which property you are after, even if there is only one property:
[PS]> $property.Name
armsvc
[PS]> $property -eq "armsvc"
False
[PS]> $property.Name -eq "armsvc"
True
[PS]> $property.Name.GetType().FullName
System.String
As per other answers here, if you want to use a single property within a string, you need to evaluate the expression (put brackets around it) and prefix with a dollar sign ($) to declare the expression dynamically as a variable to be inserted into the string:
[PS]> "The first process in the list is: $($property.Name)"
The first process in the list is: armsvc
Quite correctly, others have answered this question by recommending the -ExpandProperty parameter for the Select-Object cmdlet. This bypasses some of the headache by returning the value of the property specified, but you will want to use different approaches in different scenarios.
-ExpandProperty <String>
Specifies a property to select, and indicates that an attempt should
be made to expand that property
https://technet.microsoft.com/en-us/library/hh849895.aspx
[PS]> (Get-Process)[0] | Select-Object -ExpandProperty Name
armsvc
powershell variables
Try this :
$obj = #{
SomeProp = "Hello"
}
Write-Host "Property Value is $($obj."SomeProp")"
Here is an alternative way to get an object's property value:
write-host $(get-something).SomeProp
$com1 = new-object PSobject #Task1
$com2 = new-object PSobject #Task1
$com3 = new-object PSobject #Task1
$com1 | add-member noteproperty -name user -value jindpal #Task2
$com1 | add-member noteproperty -name code -value IT01 #Task2
$com1 | add-member scriptmethod ver {[system.Environment]::oSVersion.Version} #Task3
$com2 | add-member noteproperty -name user -value singh #Task2
$com2 | add-member noteproperty -name code -value IT02 #Task2
$com2 | add-member scriptmethod ver {[system.Environment]::oSVersion.Version} #Task3
$com3 | add-member noteproperty -name user -value dhanoa #Task2
$com3 | add-member noteproperty -name code -value IT03 #Task2
$com3 | add-member scriptmethod ver {[system.Environment]::oSVersion.Version} #Task3
$arr += $com1, $com2, $com3 #Task4
write-host "windows version of computer1 is: "$com1.ver() #Task3
write-host "user name of computer1 is: "$com1.user #Task6
write-host "code of computer1 is: "$com1,code #Task5
write-host "windows version of computer2 is: "$com2.ver() #Task3
write-host "user name of computer2 is: "$com2.user #Task6
write-host "windows version of computer3 is: "$com3.ver() #Task3
write-host "user name of computer3 is: "$com1.user #Task6
write-host "code of computer3 is: "$com3,code #Task5
read-host

Method to export objects with varying properties?

I hit a common problem with my scripting lately and decided to throw it into the wild to see how other people deal with this problem.
tl;dr; I want to export objects which have a varying number of properties. eg; object 1 may have 3 IP address but object 2 has 7 IP addresses.
I've evolved to creating a new object with custom properties and then injecting these objects into an array as my method of catching results - happy to hear if there is a better way but this is how I roll. This method works 100% when outputting to the screen as the objects are shown in list format - I've tried a number of export/out-file methods to no avail when I want to capture and store the output for reading in something like Excell.
The following is an example of me building an object and storing it (the function of the code is not important here - just the results it generates):
add-pssnapin Quest.ActiveRoles.ADManagement
$Groups = get-qadgroup AmbigousGroupNameHere-
$UserInfo = #()
ForEach ( $Group in $Groups ) {
$CurrentGroupMembers = get-qadgroupmember $Group
write-host "Processing group:" $Group
ForEach ( $GroupMember in $CurrentGroupMembers ) {
If ( $GroupMember.type -eq "User" ) {
$counter = 1
write-host "Processing member:" $GroupMember
$UserObject = get-qaduser $GroupMember | select SamAccountName,FirstName,LastName
$objUserInfo = New-Object System.Object
$objUserInfo | Add-Member -MemberType NoteProperty -Name SamAccountName -Value $UserObject.SamAccountName
$objUserInfo | Add-Member -MemberType NoteProperty -Name FirstName -Value $UserObject.FirstName
$objUserInfo | Add-Member -MemberType NoteProperty -Name LastName -Value $UserObject.LastName
$GroupMembership = get-qadgroup -ContainsMember $GroupMember | where name -like "AmbigousGroupNameHere-*"
ForEach ( $GroupName in $GroupMembership ) {
$objUserInfo | Add-Member -MemberType NoteProperty -Name CtxGroup$counter -Value $GroupName.SamAccountName
$counter++
}
$UserInfo += $objUserInfo
} else {
write-host "This is a group - we are ignoring it."
}
}
}
$UserInfo | Export-Csv UsersOutput.csv -NoType
From the above - you can see I scale the object property name by 1 for each group. CtxGroup$counter allows me to scale an object for the correct number of groups each user has. Confirmed this works great when outputting to the screen by default. The object is listed and I can see a new property for each group that matches for that user.
Now for the problem. When I export-csv or out-file the file is generated with enough headers based off the first object - so it creates the headings based on the amount of properties the first object has. So lets say the first user has 3 matching groups, it will create heading CtxGroup1, CtxGroup2, CtxGroup3. Great! No.
If the next user has 5 matching groups - only the first three are included in the output and the additional 2 are discarded as we don't have headings for CtxGroup4, CtxGroup5.
How on earth do other people deal with this?
side note; I considered creating my first object as a dummy with a massive amount of object (and hence headings) but well - that is not cool and really makes me feel inefficient.
You can obtain what you want ordering $UserInfo array by the number of properties, it can be done, but it's not so simple, in your case I would add another propertie with the count of groups added:
...
ForEach ( $GroupName in $GroupMembership ) {
$objUserInfo | Add-Member -MemberType NoteProperty -Name CtxGroup$counter -Value $GroupName.SamAccountName
$counter++
}
$objUserInfo | Add-Member -MemberType NoteProperty -Name NbCtxGroup -Value ($counter - 1)
$UserInfo += $objUserInfo
...
And then order descending the array on this propertie :
$UserInfo | Sort-Object -Property NbCtxGroup -Descending | Export-Csv .\F.csv
It's not so nice but it will do the trick.
Have a look at :
$objUserInfo.psobject.Properties | Measure-Object