I have an array, with a nested hashtable containing data:
tid : 1
token : 12345
participant_info : #{firstname=bob; lastname=smith; email=bob.smith#email.com}
completed : y
tid : 2
token : 67890
participant_info : #{firstname=Alice; lastname=Jones; email=alice.jones#email.com}
completed : n
I would like to output this as a CSV with the nested items of firstname, surname and email pulled out of the inner hashtable - eg
tid,token,firstname,surname,email,completed
1,12345,bob,smith,bob.smith#email.com,y
2,67890,alice,jones,alice.jones#email.com,n
I'm going to guess that the answer is looping through with foreach and creating a custom ps object, but because my nested items aren't named, I can't work out how to do this using the other examples on here.
Any assistance is appreciated! Thanks!
Given your sample:
#(
[pscustomobject]#{
tid = 1
token = 12345
participant_info = #{
firstname = 'bob'
lastname = 'smith'
email = 'bob.smith#email.com'
}
completed = 'N'
}
...
)
And desired output:
id,token,firstname,surname,email,completed
1,12345,bob,smith,bob.smith#email.com,y
2,67890,alice,jones,alice.jones#email.com,n
You can do something like this:
$JsonList = #( ... )
$Path = "$Env:UserProfile\file.csv"
ForEach ($Sample in $JsonList)
{
$Sample | Select-Object -Property #(
#{N = 'id'; E = { $Sample.tid }}
#{N = 'token'; E = { $Sample.token }}
#{N = 'firstname'; E = { $Sample.participant_info.firstname }}
#{N = 'surname'; E = { $Sample.participant_info.lastname }}
#{N = 'email'; E = { $Sample.participant_info.email }}
#{N = 'completed'; E = { $Sample.completed }}
) | Export-Csv -Path $Path -Append
}
Edit: you're dealing with PSCustomObject, not Hashtable, so my previous syntax won't work for that. Here's what I'm presuming your code to look like now (I've updated my above example):
#"
[
{
"tid": 1,
"token": 12345,
"participant_info": {
"firstname": "bob",
"lastname": "smith",
"email": "bob.smith#email.com"
},
"completed": "N"
}
...
]
"# | ConvertFrom-Json
Related
I am attempting to display information for administrators processing user changes to easily see what changes are being made to certain fields in Active Directory users. I've written a PowerShell script to process these changes, but I'm having trouble formatting the output for the administrators who are running the script. See this post for a similar, but different problem.
My code:
$output = [pscustomobject]#{'UserName' = $user.samAccountName; 'FirstName' = $user.GivenName;'LastName' = $user.Surname;'DisplayName' = $user.DisplayName;'UPN' = $user.UserPrincipalName;'E-Mail_Address' = $user.proxyAddresses;'Title' = $user.Title;'Office' = $user.PhysicalDeliveryOfficeName;'Dept' = $user.Department;}
$output[$index++] | Add-Member -NotePropertyMembers #{'NewUserName' = $samAccountName; 'NewFirstName' = $firstName;'NewLastName' = $lastname;'NewDisplayName' = $displayName;'NewUPN' = $UPN;'NewE-Mail_Address' = $SMTP1;'NewTitle' = $Title;'Newoffice' = $office;'NewDept' = $department;}
$output
Output:
UserName : FirstLast
FirstName : First
LastName : Last
DisplayName : First Last
UPN : FirstLast#company.com
E-Mail_Address : SMTP:FirstLast#company.com
Title : CEO
Office : HQ
Dept : Executives
NewDept : Maintenance
NewE-Mail_Address : SMTP:FirstLast#company.com
NewOffice : The Dump
NewFirstName : First
NewLastName : Last
NewUserName : FirstLast
NewDisplayName : First Last
NewUPN : FirstLast#company.com
NewTitle : Trash Collector
Desired Output:
UserName : FirstLast NewUserName : FirstLast
FirstName : First NewFirstName : First
LastName : Last NewLastName : Last
DisplayName : First Last NewDisplayName : First Last
UPN : FirstLast#company.com NewUPN : FirstLast#company.com
E-Mail_Address : SMTP:FirstLast#company.com NewE-Mail_Address : SMTP:FirstLast#company.com
Title : CEO NewTitle : Trash Collector
Office : HQ Newoffice : The Dump
Dept : Executives NewDept : Maintenance
Below will create the two Format-List outputs next to eachother.
Because more than likely this will not fit the width of the console screen, the output is captured first and output using Out-String with a high number for parameter -Width.
Like this, you can also save to text file.
$outputOld = [pscustomobject]#{
'UserName' = $user.samAccountName; 'FirstName' = $user.GivenName;
'LastName' = $user.Surname;'DisplayName' = $user.DisplayName;
'UPN' = $user.UserPrincipalName;'E-Mail_Address' = $user.proxyAddresses;
'Title' = $user.Title;'Office' = $user.PhysicalDeliveryOfficeName;
'Dept' = $user.Department}
$outputNew = [pscustomobject]#{
'NewUserName' = $samAccountName; 'NewFirstName' = $firstName;
'NewLastName' = $lastname;'NewDisplayName' = $displayName;'NewUPN' = $UPN;
'NewE-Mail_Address' = $SMTP1;'NewTitle' = $Title;'Newoffice' = $office;
'NewDept' = $department}
# capture the Format-List output of each object and split into a string array
$outLeft = #(($outputOld | Format-List | Out-String) -split '\r?\n')
$outRight = #(($outputNew | Format-List | Out-String) -split '\r?\n')
# determine the maximum length of each string in the list that goes to the left
$maxLength = ($outLeft | Measure-Object -Property Length -Maximum).Maximum
# get the maximum number of lines
$maxLines = [math]::Max($outLeft.Count, $outRight.Count)
# collect the combined output
$result = for ($i = 0; $i -lt $maxLines; $i++) {
$left = if ($i -lt $outLeft.Count) { "{0,-$maxLength}" -f $outLeft[$i] } else { ' ' * $maxLength }
$right = if ($i -lt $outRight.Count) { $outRight[$i] } else {''}
('{0} {1}' -f $left, $right).TrimEnd()
}
# display on screen
$result | Out-String -Width 1000
# save to text file
$result | Out-String -Width 1000 | Set-Content -Path 'D:\Test\output.txt'
This doesn't produce exactly the same character-by-character output as you're after, but it does produce something similar using the built-in formatting methods without lots of stringifying of values.
First, set up some test data, which would need a small change to your sample code to generate as two objects, instead of your combined $output variable:
$oldValues = [pscustomobject] [ordered] #{
"UserName" = "FirstLast"
"FirstName" = "First"
"LastName" = "Last"
"DisplayName" = "First Last"
"UPN" = "FirstLast#company.com"
"E-Mail_Address" = "SMTP:FirstLast#company.com"
"Title" = "CEO"
"Office" = "HQ"
"Dept" = "Executives"
};
$newValues = [pscustomobject] [ordered] #{
"NewDept" = "Maintenance"
"NewE-Mail_Address" = "SMTP:FirstLast#company.com"
"NewOffice" = "The Dump"
"NewFirstName" = "First"
"NewLastName" = "Last"
"NewUserName" = "FirstLast"
"NewDisplayName" = "First Last"
"NewUPN" = "FirstLast#company.com"
"NewTitle" = "Trash Collector"
};
Next, we map the variables into an array of PropertyName, OldValue, NewValue objects:
$rows = $oldValues.psobject.properties `
| foreach-object {
$property = $_;
[pscustomobject] [ordered] #{
"PropertyName" = $property.Name
"OldValue" = $property.Value
"NewValue" = $newValues.("New" + $property.Name)
}
};
And then we can format it using the built-in cmdlets:
$rows | format-table
# PropertyName OldValue NewValue
# ------------ -------- --------
# UserName FirstLast FirstLast
# FirstName First First
# LastName Last Last
# DisplayName First Last First Last
# UPN FirstLast#company.com FirstLast#company.com
# E-Mail_Address SMTP:FirstLast#company.com SMTP:FirstLast#company.com
# Title CEO Trash Collector
# Office HQ The Dump
# Dept Executives Maintenance
The nice thing is you can adjust things like column widths, word wrap, etc just using the Format-Table parameters, or even use Format-List instead:
$rows | format-list
# ... etc ...
#
# PropertyName : Title
# OldValue : CEO
# NewValue : Trash Collector
#
# PropertyName : Office
# OldValue : HQ
# NewValue : The Dump
#
# ... etc ...
And you can filter it to show just changed values:
$rows | where-object { $_.NewValue -ne $_.OldValue } | format-table
# PropertyName OldValue NewValue
# ------------ -------- --------
# Title CEO Trash Collector
# Office HQ The Dump
# Dept Executives Maintenance
It relies on the property names being in the format "Xyz -> NewXyz", but there's ways around that if it's not true in all cases...
you can use ANSI ESCAPE codes to apply decorations. This sample is based in pscustomobject with all values, but you can create two pscustomobject to make clear the code.
Notes:
Change ESC to $([char]27) or `e in Ps6 (thank you Mr. mclayton)
I prefer use Add-Member item per item.
Tested with PowerShell 5.1 ($PSVersionTable command).
$output = [pscustomobject]#{'UserName' = 'User1'; 'FirstName' = '$user.GivenName';'LastName' = '$user.Surname';'DisplayName' = '$user.DisplayName';'UPN' = '$user.UserPrincipalName';'E-Mail_Address' = '$user.proxyAddresses';'Title' = '$user.Title';'Office' = '$user.PhysicalDeliveryOfficeName';'Dept' = '$user.Department';}
$output | Add-Member -NotePropertyMembers #{'NewUserName' = 'User2'}
$output | Add-Member -NotePropertyMembers #{'NewFirstName' = '$firstName'}
$output | Add-Member -NotePropertyMembers #{'NewLastName' = '$lastname'}
$output | Add-Member -NotePropertyMembers #{'NewDisplayName' = '$displayName'}
$output | Add-Member -NotePropertyMembers #{'NewUPN' = '$UPN'}
$output | Add-Member -NotePropertyMembers #{'NewE-Mail_Address' = '$SMTP1'}
$output | Add-Member -NotePropertyMembers #{'NewTitle' = '$Title'}
$output | Add-Member -NotePropertyMembers #{'Newoffice' = '$office'}
$output | Add-Member -NotePropertyMembers #{'NewDept' = '$department'}
# Counter for column 1 and column 2
$Counter =0
$Counter1 =0
# Temporally variables
$Text
$Label
# Do the magic
foreach( $property in $output.psobject.properties.name )
{
if ( $Counter -lt 9 )
{
$Counter = $Counter +1
$Text = $output.$property
$Label = $property
echo "ESC[$Counter;1H $Label : $Text "
} else
{
$Counter1 = $Counter1 +1
$Text = $output.$property
$Label = $property
echo "ESC[$Counter1;60H $Label : $Text "
}
}
#Move to end
echo "ESC[20;1H "
Result
Other ANSI commands
Happy coding my friends.
I have two array which have the following objects:
$dataset1 = #(
#{
MachineName = "AAA"
ID = "111"
},
#{
MachineName = "BBB"
ID = "222"
},
#{
MachineName = "CCC"
ID = "111"
},
#{
MachineName = "DDD"
ID = "333"
},
#{
MachineName = "EEE"
ID = "111"
}
)
$dataset2 = #(
#{
ID = "111"
TagName = "ALPHA2"
},
#{
ID = "222"
TagName = "ALPHA0"
},
#{
ID = "333"
TagName = "ALPHA8"
},
#{
ID = "444"
TagName = "ALPHA29"
},
)
Now I want to create an array which have an object of TagName and for each object of TagName it should contain a list of MachineNames separated by comma so something like this:
TagName | MachineName
ALPHA2 AAA,CCC,EEE
ALPHA0 BBB
ALPHA8 DDD
This is the code that I have tried:
$Joined= Foreach ($row in $dataset1)
{
[pscustomobject]#{
ID = $row.ID
MachineName = $row.MachineName -join ','
TagName = $dataset2 | Where-Object {$_.ID -eq $row.ID} | Select-Object -ExpandProperty TagName
}
}
But it is not generating a comma list of Machine names instead it is printing individual rows for each machine name.
I would iterate $dataset2 instead of $dataset1.
$joined = foreach($row in $dataset2){
[PSCustomObject]#{
TagName = $row.tagname
MachineName = ($dataset1 | Where-Object id -eq $row.id).MachineName -join ','
}
}
If you're trying to exclude those entries in $dataset1 that don't have a corresponding entry in $dataset2, change to this.
$joined = foreach($row in $dataset2 | Where-Object id -in $dataset1.id){
[PSCustomObject]#{
TagName = $row.tagname
MachineName = ($dataset1 | Where-Object id -eq $row.id).MachineName -join ','
}
}
it is not generating a comma list of Machine names instead it is printing individual rows for each machine name.
That's actually a great starting point!
Just pipe the resulting objects to Group-Object and extract the machine names from each resulting group:
$joined |Group-Object TagName |Select Name,#{Name='Machines';Expression = {$_.Group.MachineName -join ','}}
Which should give you something like:
Name Machines
---- --------
ALPHA2 AAA,CCC,EEE
ALPHA0 BBB
ALPHA8 DDD
I have a source array like this
$myArray = (
#{ id = "1"; name = "first item";
subArray = (
#{ id = "A"; name = "first subitem" },
#{ id = "B"; name = "second subitem" } )
},
#{ id = "2"; name = "second item";
subArray = (
#{ id = "C"; name = "third subitem" },
#{ id = "D"; name = "fourth subitem" } )
}
)
I need to extract the relations between the parent and child arrays like following:
source target
-----------------
1 A
1 B
2 C
2 D
I have come up with a following code to achieve that
$myArray | ForEach-Object {
$id = $_.id
$_.subArray | ForEach-Object {
#{
source = $id
target = $_.id
}
}
}
I wonder if there is some more straight forward solution.
Edit:
Based on Marsze answer - slightly modified solution
$myArray| ForEach-Object
{$a=$_; $a.subArray | Select-Object #{n="source";e={$a.id}},#{n="target";e={$_.id}}
}
Looks pretty straightforward to me. You could use foreach loops instead of the pipeline cmdlet, which is faster and has the advantage, that you can reference variables on each level directly.
Also I recommended converting to PSCustomObject for the proper output format:
foreach ($a in $myArray) {
foreach ($b in $a.subArray) {
[PSCustomObject]#{source = $a.id; target = $b.id}
}
}
Alternatively with New-Object:
foreach ($a in $myArray) {
foreach ($b in $a.subArray) {
New-Object PSObject -Property #{source = $a.id; target = $b.id}
}
}
Or the Select-Object version:
foreach ($a in $myArray) {
$a.subArray | select #{n="source";e={$a.id}},#{n="target";e={$_.id}}
}
Question: How do I write this so it gives the same result in v4 and v5?
I am trying to group the following dataset by SiteCode.
I have a dataset as follows [Array of Hashes]:
Assume AppointmentId is always unique
$groupedDataset = #{}
$dataset = #(
#{
Program = "x"
AppointmentId = "1234567891"
AdminDate = "x"
CountryName = "x"
SiteCode = "x1111"
DateRequested = "x"
SubjectID = "x"
AccountID = "x"
},
#{
Program = "x"
AppointmentId = "1234567892"
AdminDate = "x"
CountryName = "x"
SiteCode = "x1112"
DateRequested = "x"
SubjectID = "x"
AccountID = "x"
},
#{
Program = "x"
AppointmentId = "1234567893"
AdminDate = "x"
CountryName = "x"
SiteCode = "x1113"
DateRequested = "x"
SubjectID = "x"
AccountID = "x"
},
#{
Program = "x"
AppointmentId = "1234567894"
AdminDate = "x"
CountryName = "x"
SiteCode = "x1111"
DateRequested = "x"
SubjectID = "x"
AccountID = "x"
}
)
When I run the following code below in PS Version: 5.1
$dataset |
ForEach-Object { [PSCustomObject]$_ } |
Group-Object -Property SiteCode |
ForEach-Object {
$groupedDataset[$_.Name] = $_.Group
}
It returns the result I require:
Name Value
---- -----
x1113 {#{SiteCode=x1113; Program=x; Appointment...
x1111 {#{SiteCode=x1111; Program=x; Appointment...
x1112 {#{SiteCode=x1112; Program=x; Appointment...
When I run the exact code in PS Version: 4.0 it returns the following:
Name Value
---- -----
{System.Collections.Hashtable, System.Collect...
In PowerShell v5.0 improvements have been made, eg. PSCustomObject casting, which is in your script
ForEach-Object { [PSCustomObject]$_ }
Try doing it the old school way and instead of building a hashtable yourself, use the one returned by the groupby.
$groupedDataset = $dataset |
ForEach-Object {
[PSCustomObject]#{
Program = $_.Program
AppointmentId = $_.AppointmentId
AdminDate = $_.AdminDate
CountryName = $_.CountryName
SiteCode = $_.SiteCode
DateRequested = $_.DateRequested
SubjectID = $_.SubjectID
AccountID = $_.AccountID
}
} |
Group-Object -Property SiteCode -AsHashTable
This results in
$groupedDataset | out-host
Count Name Group
----- ----- ------
2 x1111 {#{Program=x; AppointmentId=1234567891; AdminDate=x; CountryName=x; SiteCode=x1111; DateRequested=x; SubjectID=x; AccountID=x}, #{Program=x; AppointmentId=1234567894; AdminDate=x; CountryN...
1 x1112 {#{Program=x; AppointmentId=1234567892; AdminDate=x; CountryName=x; SiteCode=x1112; DateRequested=x; SubjectID=x; AccountID=x}}
1 x1113 {#{Program=x; AppointmentId=1234567893; AdminDate=x; CountryName=x; SiteCode=x1113; DateRequested=x; SubjectID=x; AccountID=x}
Why is this coming up blank for all machines in the passwordlastset attribute, when I export it in the csv file? Everything else works perfectly.
$Searcher = New-ObjectSystem.DirectoryServices.DirectorySearcher([ADSI]"LDAP://dc=amers,dc=jhe,dc=domain,dc=com")
$Searcher.Filter = "(&(objectCategory=computer)(objectClass=computer)(!UserAccountControl:1.2.840.113556.1.4.803:=2)(operatingSystem=Windows XP*))"
$Searcher.PageSize = 100000
$results = $Searcher.Findall()
$results | ForEach-Object { $_.GetDirectoryEntry() } |
select #{ n = 'CN'; e = { ($_.CN) } },
#{ n = 'DistinguishedName'; e = { $_.DistinguishedName } },
#{ n = 'extensionattribute7'; e = { $_.extensionattribute7 } },
#{ n = 'LastLogon'; e = { [DateTime]::FromFileTime($_.PasswordLastSet) } },
#{ n = 'OperatingSystem'; e = { $_.OperatingSystem } } |
Export-Csv 'C:\temp\WindowsXP_Only.csv' -NoType -Force
By default not all properties are returned, so you need to specify the additional properties you want.
Also, if you're looking for the last logon date (per your output), you should be using lastLogonTimestamp and not PasswordLastSet.
Here's an example using Get-ADComputer, which I greatly prefer over using older methods of searching AD. Just add your Export-CSV when you're happy with the results.
$results = get-adcomputer -Filter "operatingSystem -like 'Windows XP*'" -properties cn,lastlogontimestamp,operatingsystem,extensionattribute7,PasswordLastSet -searchbase "dc=amers,dc=jhe,dc=domain,dc=com";
$results |
select #{ n = 'CN'; e = { ($_.cn) } },
#{ n = 'DistinguishedName'; e = { $_.DistinguishedName } },
#{ n = 'extensionattribute7'; e = { $_.extensionattribute7 } },
#{ n = 'LastLogon'; e = { [DateTime]::FromFileTime($_.lastLogonTimestamp) } },
#{ n = 'PasswordLastSet'; e = { [DateTime]::FromFileTime($_.PasswordLastSet) } },
#{ n = 'OperatingSystem'; e = { $_.OperatingSystem } }
You might also find this script useful