PowerShell to Create Custom Object from Existing Object - arrays

I'm attempting to create a custom object from an existing object. However, I can't seem to figure out the best way to go about doing this.
My goal is to create a custom object for each computer name that will have all of the values from Application Name.
I'm not sure if I should use a foreach or a where clause to make this happen and I'm also not sure the correct syntax to use either.
Thanks in advance for any help with this.
The data in the existing object looks like:
Computer Name, IP Address, Application Name
host1,10.10.10.10,firefox
host1,10.10.10.10,chrome
host1,10.10.10.10,internet explorer
host2,11.11.11.11,firefox
host2,11.11.11.11,chrome
host2,11.11.11.11,opera
Code Example:
foreach ($global:ComputerName in $global:SNWReportObject."Computer name"){
if ($_ -eq $global:ComputerName) {
Add-Member -InputObject $global:NEWReportObject -MemberType NoteProperty -Name ApplicationName -Value $global:SNWReportObject."Application name"
}
}
Results I'm hoping for:
Computer Name, Application Name
host1,firefox,chrome,internet explorer
host2,firefox,chrome,opera

Edit:
So your actual case was a bit weird since we're working with arrays of strings in a custom object rather than a hash table. The following code snippet will turn your object into a [HashTable] that is easy to work with. Assumption: all the input data is this well-formed
<# Example.csv
Computer name,IP-Address,Application
host1,10.10.10.10,firefox
host1,10.10.10.10,chrome
host1,10.10.10.10,internet explorer
host2,11.11.11.11,firefox
host2,11.11.11.11,chrome
host2,11.11.11.11,opera
#>
$Csv = Import-Csv -LiteralPath C:\Temp\Example.csv
[HashTable]$Hash=#{}
For ($i = 0; $i -lt ($Csv.'Computer name').Count; $i++)
{
If ($Hash.ContainsKey($Csv.'Computer name'[$i]))
{
$Hash.($Csv.'Computer name'[$i]).Application += $Csv.Application[$i]
Continue
}
$Hash.($Csv.'Computer name'[$i]) = #{
Application = #($Csv.Application[$i])
}
}
Now your members are accessible like so: $Hash.Host1.Application
To get your desired output:
Set-Content -Path C:\Temp\file.csv -Value 'Computer Name,Application Name'
$Hash.Keys |
ForEach-Object
{
Add-Content -Path C:\temp\file.csv -Value "$_,$($Hash.$_.Application -join ',')"
}
When working with a [PSCustomObject], your members are quite limited in what you can do. Here are the standard members (to ignore):
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()
Knowing this, you can parse through the rest to copy:
$Comp = #('Equals','GetHashCode','GetType','ToString'
[HashTable]$CopyObject=#{}
($Object | Get-Member).Name |
ForEach-Object {
If ($_ -notin $Comp) {$CopyObject.$_ = $Object.$_}
}

Related

Powershell nested JSON to csv conversion

I have a rather peculiar nested JSON where in some instances a key - value pair occurs as normal, but in others the type of the key appears in a further nesting.
{"metadata":{"systemId":"da1895","legalEntity":"A0"},"recordContent":{"positionDate":"2019-04-08 00:00:00.0","account":{"string":"G32"},"seg":{"string":"S"},"strike":{"double":4.4}}}
{"metadata":{"systemId":"45364d","legalEntity":"5G"},"recordContent":{"positionDate":"2019-04-08 00:00:00.0","account":{"string":"G81"},"seg":{"string":"S"},"strike":{"double":5.0}}}
In the example you can see metadata's fields are straightforward key-value pairs, but underneath recordContent, we have positionDate which is a straightforward key-value but "account":{"string":"G32"} and "strike":{"double":4.4} are not.
I'd like to ditch the type information and arrive at a CSV structure as follows:
systemId, legalEntity, positionDate, account,seg,strike
da1895, A0, 2019-04-08 00:00:00.0,G32, S, 4.4
4536d, 5G, 2019-04-08 00:00:00.0,G81, S, 5.0
Any ideas on how to convert such a structure to CSV using Powershell?
Here's what I tried:
$TemplateParametersFile = "c:\data\output.json"
$JsonParameters = Get-Content $TemplateParametersFile | ConvertFrom-Json
$metadatafields = $JsonParameters.metadata[0].PSObject.Properties.Name
$recordcontentfields = $JsonParameters.recordContent[0].PsObject.Properties.Name
$oData = New-Object PSObject
$metadatafields |
ForEach {
Add-Member -InputObject $oData -NotePropertyName ($_) -NotePropertyValue $JsonParameters.metadata.($_)
}
$recordcontentfields |
ForEach {
Add-Member -InputObject $oData -NotePropertyName ($_) -NotePropertyValue $JsonParameters.recordContent.($_)
}
This gave me:
$oData
systemId : {da1895, 45364d}
legalEntity : {A0, 5G}
positionDate : {2019-04-08 00:00:00.0, 2019-04-08 00:00:00.0}
account : {#{string=G32}, #{string=G81}}
seg : {#{string=S}, #{string=S}}
strike : {#{double=4.4}, #{double=5.0}}
I'm a bit stuck now and the above doesn't convert to csv.
Note that other than metadata and recordContent, I've not hardcoded any fieldnames and I'd like to maintain that flexibility in case the JSON structure changes.
Thanks
I suggest collecting the property-name-value pairs iteratively in an ordered hashtable ([ordered] #{}), which can then be cast to [pscustomobject] to convert it to a custom object.
No property names are hard-coded in the following solution, but the object-graph structure is assumed to follow the pattern in your sample JSON, which is limited to one level of nesting - if you need to process arbitrarily nested objects, this answer may be a starting point.
Reflection (discovery of the property names and values) is performed via the intrinsic .psobject property that PowerShell makes available on all objects.
# Parse sample JSON into an array of [pscustomobject] graphs.
$fromJson = ConvertFrom-Json #'
[
{"metadata":{"systemId":"da1895","legalEntity":"A0"},"recordContent":{"positionDate":"2019-04-08 00:00:00.0","account":{"string":"G32"},"seg":{"string":"S"},"strike":{"double":4.4}}}
,
{"metadata":{"systemId":"45364d","legalEntity":"5G"},"recordContent":{"positionDate":"2019-04-08 00:00:00.0","account":{"string":"G81"},"seg":{"string":"S"},"strike":{"double":5.0}}}
]
'#
# Initialize an aux. ordered hashtable to collect the property-name-value
# pairs in.
$oht = [ordered] #{}
$fromJson | ForEach-Object {
$oht.Clear()
# Loop over top-level properties.
foreach ($topLevelProp in $_.psobject.Properties) {
# Loop over second-level properties.
foreach ($prop in $topLevelProp.Value.psobject.Properties) {
if ($prop.Value -is [System.Management.Automation.PSCustomObject]) {
# A nested value: Use the value of the (presumed to be one-and-only)
# property of the object stored in the value.
$oht[$prop.Name] = $prop.Value.psobject.Properties.Value
}
else {
# A non-nested value: use as-is.
$oht[$prop.Name] = $prop.Value
}
}
}
# Construct and output a [pscustomobject] from the aux. ordered hashtble.
[pscustomobject] $oht
} |
ConvertTo-Csv # Replace this with Export-Csv to export to a file.
The above yields:
"systemId","legalEntity","positionDate","account","seg","strike"
"da1895","A0","2019-04-08 00:00:00.0","G32","S","4.4"
"45364d","5G","2019-04-08 00:00:00.0","G81","S","5"
A few years ago, I wrote a reusable Flatten-Object function for this.
The only difference is that it combines the (sub)property names with the parent property names as they might not be unique:
$JsonParameters |Flatten-Object |Format-Table
metadata.systemId metadata.legalEntity recordContent.positionDate recordContent.account.string recordContent.seg.string recordContent.strike.double
----------------- -------------------- -------------------------- ---------------------------- ------------------------ ---------------------------
da1895 A0 2019-04-08 00:00:00.0 G32 S 4.4
45364d 5G 2019-04-08 00:00:00.0 G81 S 5
Try this:
$data = ConvertFrom-Json #"
[
{"metadata":{"systemId":"da1895","legalEntity":"A0"},"recordContent":{"positionDate":"2019-04-08 00:00:00.0","account":{"string":"G32"},"seg":{"string":"S"},"strike":{"double":4.4}}},
{"metadata":{"systemId":"45364d","legalEntity":"5G"},"recordContent":{"positionDate":"2019-04-08 00:00:00.0","account":{"string":"G81"},"seg":{"string":"S"},"strike":{"double":5.0}}}
]
"#
$data | Select-Object -Property #{l="systemId"; e={$_.metadata.systemId}}, #{l="legalEntity"; e={$_.metadata.legalEntity}},
#{l="positionDate"; e={$_.recordContent.positionDate}}, #{l="account"; e={$_.recordContent.account.string}},
#{l="seg"; e={$_.recordContent.seg.string}}, #{l="strike"; e={$_.recordContent.strike.double}} | Export-Csv
This should work with any nested psobject.
$json = #'
{"metadata":{"systemId":"da1895","legalEntity":"A0"},"recordContent":{"positionDate":"2019-04-08 00:00:00.0","account":{"string":"G32"},"seg":{"string":"S"},"strike":{"double":4.4}}}
'#
$obj = ConvertFrom-Json $json
$obj.recordContent | gm -MemberType NoteProperty | % {
$prop = $_.name
if ($obj.recordContent.$prop.GetType().name -eq 'pscustomobject') {
$obj.recordContent.$prop = $obj.recordContent.$prop.psobject.Members | where membertype -eq noteproperty | select -ExpandProperty value
}
$obj.metadata | add-member -MemberType NoteProperty -Name $prop -Value $obj.recordContent.$prop
}
$newobj = $obj.metadata
$newobj

Create a String array from an object array in powershell

Im getting the names of these computers and putting them into an array. Now what i want to do is to convert them into a string array to be able to check which policy they are on using a Get-ADComputer for loop or using a foreach loop (Can you recommend which one to use)
$global:arrComputers = #()
$computerStrings = Get-ADComputer -Filter 'SamAccountName -like "*Name*"' | Select -Expand Name
foreach ($line in $computerStrings)
{
$a = $line.ToString()
$b = $a.split()
$temp = #{}
$temp = New-Object object
$temp | Add-Member -MemberType "noteproperty" -Name Name -Value $b[0]
$global:arrComputers += $temp
}
$global:arrComputers
This is the command i want to run to check the policy they are under
Get-ADComputer "Name" -Properties MemberOf | %{if ($_.MemberOf -like "*POLICY_NAME*") {Write-Host "ON"} else {Write-Host "NOT ON"}}
I have tested both blocks of code and they are working the only problem im having is turning that array into a string array. I also tried the ToString() To be able to loop through it with the Get-ADComputer "Name"
"memberOf" property in objects returned by "Get-ADComputer" returns a list of strings containing Distinguished Name of each group this computer is a member of.
Therefore, I assume when you say "This is the command i want to run to check the policy they are under", you are referring to a group membership that a group policy is targeting right?
Below code then will do it:
$computers = #();
Get-ADComputer -Filter * -Properties Name,MemberOf | %{if ($_.MemberOf -like "*computer_group_name*") { $computers += $_.Name } }
Explanation:
First line, define an array $computers
Second line, query AD for computer object properties Name,MemberOf
then, $_.MemberOf contains group name in string, add Name property(string) to array of strings you defined on line 1

Generically Transpose PowerShell Array Rows into Columns

There are quite a few posts on SO that address PowerShell transposition. However, most of the code is specific to the use case or addresses data being gathered from a text/CSV file and does me no good. I'd like to see a solution that can do this work without such specifics and works with arrays directly in PS.
Example data:
Customer Name: SomeCompany
Abbreviation: SC
Company Contact: Some Person
Address: 123 Anywhere St.
ClientID: XXXX
This data is much more complicated, but I can work with it using other methods if I can just get the rows and columns to cooperate. The array things that "Name:" and "SomeCompany" are column headers. This is a byproduct of how the data is gathered and cannot be changed. I'm importing the data from an excel spreadsheet with PSExcel and the spreadsheet format is not changeable.
Desired output:
Customer Name:, Abbreviation:, Company Contact:, Address:, ClientID:
SomeCompany, SC, Some Person, 123 Anywhere St., XXXX
Example of things I've tried:
$CustInfo = Import-XLSX -Path "SomePath" -Sheet "SomeSheet" -RowStart 3 -ColumnStart 2
$b = #()
foreach ($Property in $CustInfo.Property | Select -Unique) {
$Props = [ordered]#{ Property = $Property }
foreach ($item in $CustInfo."Customer Name:" | Select -Unique){
$Value = ($CustInfo.where({ $_."Customer Name:" -eq $item -and
$_.Property -eq $Property })).Value
$Props += #{ $item = $Value }
}
$b += New-Object -TypeName PSObject -Property $Props
}
This does not work because of the "other" data I mentioned. There are many other sections in this particular workbook so the "Select -Unique" fails without error and the output is blank. If I could limit the input to only select the rows/columns I needed, this might have a shot. It appears that while there is a "RowStart" and "ColumnStart" to Import-XLSX, there are no properties for stopping either one.
I've tried methods from the above linked SO questions, but as I said, they are either too specific to the question's data or apply to importing CSV files and not working with arrays.
I was able to resolve this by doing two things:
Removed the extra columns by using the "-Header" switch on the Import-XLSX function to add fake header names and then only select those headers.
$CustInfo = Import-XLSX -Path "SomePath" -Sheet "SomeSheet" -RowStart 2 -ColumnStart 2 -Header 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18 | Select "1","2"
The downside to this is that I had to know how many columns the input data had -- Not dynamic. If anyone can provide a solution to this issue, I'd be grateful.
Flipped the columns and headers with a simple foreach loop:
$obj = [PSCustomObject]#{}
ForEach ($item in $CustInfo) {
$value = $null
$name = $null
if ($item."2") { [string]$value = $item."2" }
if ($item."1") { [string]$name = $item."1" }
if ($value -and $name) {
$obj | Add-Member -NotePropertyName $name -NotePropertyValue $value
}
}
I had to force string type on the property names and values because the zip codes and CustID was formatting as an Int32. Otherwise, this does what I need.

Update object array over multiple iterations

I have an array of custom objects:
$report = #()
foreach ($person in $mylist)
{
$objPerson = New-Object System.Object
$objPerson | Add-Member -MemberType NoteProperty -Name Name -Value $person.Name
$objPerson | Add-Member -MemberType NoteProperty -Name EmployeeID
$objPerson | Add-Member -MemberType NoteProperty -Name PhoneNumber
$report += $objPerson
}
Note that I haven't set values for the last two properties. The reason I've done this is because I'm trying to produce a matrix where I'll easily be able to see where these are blanks (although I could just set these to = "" if I have to).
Then, I want to iterate through a second dataset and update these values within this array, before exporting the final report. E.g. (this bit is pretty much pseudo code as I have no idea how to do it:
$phonelist = Import-Csv .\phonelist.csv
foreach ($entry in $phonelist)
{
$name = $entry.Name
if ($report.Contains(Name))
{
# update the PhoneNumber property of that specific object in the array with
# another value pulled out of this second CSV
}
else
{
# Create a new object and add it to the report - don't worry I've already got
# a function for this
}
}
I'm guessing for this last bit I probably need my if statement to return an index, and then use that index to update the object. But I'm pretty lost at this stage.
For clarity this is a simplified example. After that I need to go through a second file containing the employee IDs, and in reality I have about 10 properties that need updating all from different data sources, and the data sources contain different lists of people, but with some overlaps. So there will be multiple iterations.
How do I do this?
I would read phonelist.csv into a hashtable, e.g. like this:
$phonelist = #{}
Import-Csv .\phonelist.csv | ForEach-Object { $phonelist[$_.name] = $_.number }
and use that hashtable for filling in the phone numbers in $report as you create it:
$report = foreach ($person in $mylist) {
New-Object -Type PSObject -Property #{
Name = $person.Name
EmployeeID = $null
PhoneNumber = $phonelist[$person.Name]
}
}
You can still check the phone list for entries that are not in the report like this:
Compare-Object $report.Name ([array]$phonelist.Keys) |
Where-Object { $_.SideIndicator -eq '=>' } |
Select-Object -Expand InputObject
I would iterate over the $phonelist two times. The first time, you could filter all phone entities where the name is in your $myList and create the desired object:
$phonelist = import-cse .\phonelist.csv
$report = $phonelist | Where Name -in ($mylist | select Name) | Foreach-Object {
[PSCustomObject]#{
Name = $_.Name
PhoneNumber = $_.PhoneNumber
EmployeeID = ''
}
}
The second time you filter all phone entities where the name is not in $myList and create the new object:
$report += $phonelist | Where Name -NotIn ($mylist | select Name) | Foreach-Object {
#Create a new object and add it to the report - don't worry I've already got a function for this
}

Select-String bug splitting up lines [duplicate]

I'm using Powershell to set up IIS bindings on a web server, and having a problem with the following code:
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
if ($serverIps.length -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
$primaryIp = $serverIps[0]
$secondaryIp = $serverIps[1]
If there's 2+ IPs on the server, fine - Powershell returns an array, and I can query the array length and extract the first and second addresses just fine.
Problem is - if there's only one IP, Powershell doesn't return a one-element array, it returns the IP address (as a string, like "192.168.0.100") - the string has a .length property, it's greater than 1, so the test passes, and I end up with the first two characters in the string, instead of the first two IP addresses in the collection.
How can I either force Powershell to return a one-element collection, or alternatively determine whether the returned "thing" is an object rather than a collection?
Define the variable as an array in one of two ways...
Wrap your piped commands in parentheses with an # at the beginning:
$serverIps = #(gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort)
Specify the data type of the variable as an array:
[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
Or, check the data type of the variable...
IF ($ServerIps -isnot [array])
{ <error message> }
ELSE
{ <proceed> }
Force the result to an Array so you could have a Count property. Single objects (scalar) do not have a Count property. Strings have a length property so you might get false results, use the Count property:
if (#($serverIps).Count -le 1)...
By the way, instead of using a wildcard that can also match strings, use the -as operator:
[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration -filter "IPEnabled=TRUE" | Select-Object -ExpandProperty IPAddress | Where-Object {($_ -as [ipaddress]).AddressFamily -eq 'InterNetwork'}
You can either add a comma(,) before return list like return ,$list or cast it [Array] or [YourType[]] at where you tend to use the list.
If you declare the variable as an array ahead of time, you can add elements to it - even if it is just one...
This should work...
$serverIps = #()
gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort | ForEach-Object{$serverIps += $_}
You can use Measure-Object to get the actual object count, without resorting to an object's Count property.
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
if (($serverIps | Measure).Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
Return as a referenced object, so it never converted while passing.
return #{ Value = #("single data") }
I had this problem passing an array to an Azure deployment template. If there was one object, PowerShell "converted" it to a string. In the example below, $a is returned from a function that gets VM objected according to the value of a tag. I pass the $a to the New-AzureRmResourceGroupDeployment cmdlet by wrapping it in #(). Like so:
$TemplateParameterObject=#{
VMObject=#($a)
}
New-AzureRmResourceGroupDeployment -ResourceGroupName $RG -Name "TestVmByRole" -Mode Incremental -DeploymentDebugLogLevel All -TemplateFile $templatePath -TemplateParameterObject $TemplateParameterObject -verbose
VMObject is one of the template's parameters.
Might not be the most technical / robust way to do it, but it's enough for Azure.
Update
Well the above did work. I've tried all the above and some, but the only way I have managed to pass $vmObject as an array, compatible with the deployment template, with one element is as follows (I expect MS have been playing again (this was a report and fixed bug in 2015)):
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions")
foreach($vmObject in $vmObjects)
{
#$vmTemplateObject = $vmObject
$asJson = (ConvertTo-Json -InputObject $vmObject -Depth 10 -Verbose) #-replace '\s',''
$DeserializedJson = (New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer -Property #{MaxJsonLength=67108864}).DeserializeObject($asJson)
}
$vmObjects is the output of Get-AzureRmVM.
I pass $DeserializedJson to the deployment template' parameter (of type array).
For reference, the lovely error New-AzureRmResourceGroupDeployment throws is
"The template output '{output_name}' is not valid: The language expression property 'Microsoft.WindowsAzure.ResourceStack.Frontdoor.Expression.Expressions.JTokenExpression'
can't be evaluated.."
There is a way to deal with your situation. Leave most of you code as-is, just change the way to deal with the $serverIps object. This code can deal with $null, only one item, and many items.
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
# Always use ".Count" instead of ".Length".
# This works on $null, only one item, or many items.
if ($serverIps.Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
# Always use foreach on a array-possible object, so that
# you don't have deal with this issue anymore.
$serverIps | foreach {
# The $serverIps could be $null. Even $null can loop once.
# So we need to skip the $null condition.
if ($_ -ne $null) {
# Get the index of the array.
# The #($serverIps) make sure it must be an array.
$idx = #($serverIps).IndexOf($item)
if ($idx -eq 0) { $primaryIp = $_ }
if ($idx -eq 1) { $secondaryIp = $_ }
}
}
In PowerShell Core, there is a .Count property exists on every objects. In Windows PowerShell, there are "almost" every object has an .Count property.

Resources