Is it possible to sort the output of the Format-List cmdlet by property name?
Suppose that I have an object $x with two properties "A" and "B", and when I run Format-List with it I get
(PS) > $x | Format-List
B : value b
A : value a
I would like to have
(PS) > $x | Format-List
A : value a
B : value b
NOTE: I should have specified from the beginning that, unlike in the example with "A" and "B" properties, the real object I have to deal with has quite a lot of properties, and new ones could be added in the future, so I don't know all the property names in advance.
AFAIK, Format-List does not provide such an option.
For your particular example this should work:
$x | Select-Object A, B | Format-List
If the property set is not fixed/known then the procedure will be more tricky with use of Get-Member and some preprocessing making sorted parameter array for Select-Object.
EDIT:
Here it is (let's use $host instead of $x):
$host | Select-Object ([string[]]($host | Get-Member -MemberType Property | %{ $_.Name } | Sort-Object)) | Format-List
Christopher is right, Select-Object is not absolutely needed:
$host | Format-List ([string[]]($host | Get-Member -MemberType Property | %{ $_.Name } | Sort-Object))
Nothing wrong with the accepted answer, but a really quick-and-dirty option for a one-off—that doesn't require having the collection already in a variable—might be...
... | Format-List | Out-String -Stream | Sort-Object
...which does a sort on each line of the output of Format-List.
Note that any property values that go onto the next line will be broken (and probably appear at the top of the output), but this could be fixed by the slightly-less-memorable...
... | Format-List | Out-String -Stream -Width ([Int32]::MaxValue) | Sort-Object
...at the expense of column indentation.
Of course, all object/pipeline info is lost by that Out-String call, although—considering the same is true of Format-List—you probably aren't going to care by that point.
Expanding on Christopher's idea, using get-member and format-list -Property:
$x | fl -property ($x| gm | sort name).name
The closest I can think of is to create a new psobject based off the old one but with the properties sorted e.g.:
$x | %{$obj = new-object psobject; `
$_.psobject.properties | Sort Name | `
%{Add-Member -Inp $obj NoteProperty $_.Name $_.Value}; $obj} | fl
You could get fancier and give the new psobject a typename that matches the old one, etc.
If you are dealing with a small number of properties, you can specify their order with the -Property parameter.
Here is an example:
Format-List -Property Owner, Path
If you have a lot of properties, I am not sure there is any easy way to sort them in Format-List, like Roman said.
This seems to work OK (edited so it accepts pipeline input):
function Format-SortedList
{
param (
[Parameter(ValueFromPipeline = $true)]
[Object]$InputObject,
[Parameter(Mandatory = $false)]
[Switch]$Descending
)
process
{
$properties = $InputObject | Get-Member -MemberType Properties
if ($Descending) {
$properties = $properties | Sort-Object -Property Name -Descending
}
$longestName = 0
$longestValue = 0
$properties | ForEach-Object {
if ($_.Name.Length -gt $longestName) {
$longestName = $_.Name.Length
}
if ($InputObject."$($_.Name)".ToString().Length -gt $longestValue) {
$longestValue = $InputObject."$($_.Name)".ToString().Length * -1
}
}
Write-Host ([Environment]::NewLine)
$properties | ForEach-Object {
Write-Host ("{0,$longestName} : {1,$longestValue}" -f $_.Name, $InputObject."$($_.Name)".ToString())
}
}
}
$Host, $MyInvocation | Format-SortedList
$Host, $MyInvocation | Format-SortedList -Descending
I feel sure that you can achieve the desired output. I suggest that you experiment with both Sort-Object (or plain Sort) and also Group-Object (plain Group)
My idea is to place the sort, or group before | format-list
Thus $x | sort-object -property xyz | Format-List
By using Select-Object with a calculated property (#{}) and then excluding it (-ExcludeProperty) you can also order the properties as you want. This works even when you don't know what's coming upfront.
#(
[PSCustomObject]#{
Color = 'Green'
Type = 'Fruit'
Name = 'kiwi'
Flavour = 'Sweet'
}
) | Select-Object #{Name = 'Flavour'; Expression = { $_.Flavour } },
#{Name = 'Name'; Expression = { $_.Name } }, * -ExcludeProperty Name, Flavour |
Format-List
Output:
Flavour : Sweet
Name : kiwi
Color : Green
Type : Fruit
Related
I'm fairly new to Powershell and I'm stuck with this portion of a much larger script. I need to pull all Windows Services and compare them to see if their Startup Type of Status has changed. If there were any changes, I need to count them so I can put that value in the body of an e-mail. Also, I need to attach an HTML report showing the previous and current state of the Windows services that changed.
What I've done is the following:
That piece of code generates a CSV file showing the current state of the services.
Get-Service | Select-Object -Property Name,DisplayName,StartType,ServiceType,Status | Export-Csv -Path "C:\logs\after.csv"
Then i declare two variables, one for the current state, another one for the "template", the desired state of all Windows Services.
$before = Import-Csv -Path "C:\logs\before.csv"
$after = Import-Csv -Path "C:\logs\after.csv"
Then, i compare both of them, parsing only those service that've changed and generate a CSS styled HTML report based on that
Compare-Object $before $after -Property Name,DisplayName,StartType,ServiceType,Status | ConvertTo-html -Head $css | Set-Content "C:\logs\comparison.html"
This is what i get:
This is what it should look like:
Basically, i want to show the status of the latter CSV report in a new column after the Status column of the original CSV report. And I would also like to make a row count after that, so I can send an e-mail reporting HOW MANY services suffered any changes.
Any help will be deeply appreciated.
You could use Group-Object after Compare-Object and parse out the columns you need from that.
$before = Import-Csv -Path "C:\logs\before.csv"
$after = Import-Csv -Path "C:\logs\after.csv"
# find the differences in the StartType and Status columns. Use -PassThru to be able to process further
$groups = Compare-Object -DifferenceObject $before -ReferenceObject $after -Property StartType, Status -PassThru |
Sort-Object Name | Group-Object Name
$result = foreach ($group in $groups) {
$refGroup = $after | Where-Object { $_.Name -eq $group.Name }
# output an object with new StartType_* and Status_* columns and capture that in variable $result
$group.Group[0] |
Select-Object *, #{Name = 'StartType_Before'; Expression = {$_.StartType}},
#{Name = 'StartType_After'; Expression = {$refGroup.StartType}},
#{Name = 'Status_Before'; Expression = {$_.Status}},
#{Name = 'Status_After'; Expression = {$refGroup.Status}} -ExcludeProperty StartType,Status, SideIndicator
}
# now convert the $result to HTML and add a summary line with the number of services that have changed
$result | ConvertTo-Html -Head $css -PostContent "<br />Services affected: $($result.Count)" |
Set-Content "C:\logs\comparison.html"
If you also want output in the console do:
$result | Format-Table -AutoSize
Of course it is also possible to not use Compare-Object and do like below (will be slower, but easier to understand I guess):
$before = Import-Csv -Path "C:\logs\before.csv"
$after = Import-Csv -Path "C:\logs\after.csv"
$result = foreach ($item in $before) {
$diff = $after | Where-Object { $_.Name -eq $item.Name -and
($_.StartType -ne $item.StartType -or $_.Status -ne $item.Status) }
if ($diff) {
$item | Select-Object *, #{Name = 'StartType_Before'; Expression = {$item.StartType}},
#{Name = 'StartType_After'; Expression = {$diff.StartType}},
#{Name = 'Status_Before'; Expression = {$item.Status}},
#{Name = 'Status_After'; Expression = {$diff.Status}} -ExcludeProperty StartType,Status
}
}
# output to console
$result | Format-Table -AutoSize
# convert to HTML
$result | ConvertTo-Html -Head $css -PostContent "<br />Services affected: $($result.Count)" |
Set-Content "C:\logs\comparison.html"
Output on screen will look something like
Name DisplayName ServiceType StartType_Before StartType_After Status_Before Status_After
---- ----------- ----------- ---------------- --------------- ------------- ------------
AarSvc_8246b1 Agent Activation Runtime_8246b1 224 Manual Automatic Stopped Stopped
AdobeARMservice Adobe Acrobat Update Service Win32OwnProcess Automatic Automatic Running Stopped
ALG Application Layer Gateway Service Win32OwnProcess Manual Automatic Stopped Stopped
WdNisSvc Microsoft Defender Antivirus Network Inspection Service Win32OwnProcess Manual Manual Running Stopped
I've been tasked with creating a script that checks to see if the office cameras we've set up have stopped uploading their feeds to the "Camera" share located on our Windows 2016 storage server. If the NEWEST .mkv is over an hour old compared to the current time (get-date) then the "problem" camera needs to be restarted manually. (No need to script that part.)
Here's what my Director has written so far:
#Variable Definitions start here
$numhours = 1
Get-ChildItem "d:\Shares\Cameras" | Foreach {
$folderToLookAt = ($_.FullName + "\*.mkv")
$result = Get-ChildItem -Recurse $folderToLookAt | Sort-Object CreationTime -Descending
echo $result[0].FullName
echo $result[0].CreationTime
}
The first variable really isn't used yet, but I'm kind of dumb-struck as what to do next. The above returns the full names and creation times successfully of the newest .mkvs
Suggestions on the next part?
Invert the logic - instead of searching all the files, sorting them, finding the most recent, and checking the date, do it the other way round.
Look for files created since the cutoff, and alert if there were none found:
$cutOffTime = [datetime]::Now.AddHours(-1)
Get-ChildItem "d:\Shares\Cameras" | Foreach {
$folderToLookAt = ($_.FullName + "\*.mkv")
$result = Get-ChildItem -Recurse $folderToLookAt | Where-Object { $_.CreationTime -gt $cuttoffTime }
if (-not $result)
{
"$($_.Name) has no files since the cutoff time"
}
}
I'm assuming your paths look like:
D:\Shares\Cameras\Camera1\file1.mkv
D:\Shares\Cameras\Camera1\file2.mkv
D:\Shares\Cameras\Camera2\file1.mkv
D:\Shares\Cameras\Camera2\file2.mkv
D:\Shares\Cameras\Camera3\file1.mkv
.
.
.
If so, I would do something like this:
# The path to your files
$CameraShareRoot = 'D:\Shares\Cameras';
# Number of Hours
$NumberOfHours = 1;
# Date and time of significance. It's $NumberOfHours in the past.
$MinFileAge = (Get-Date).AddHours( - $NumberOfHours);
# Get all the folders at the camera share root
Get-ChildItem -Path $CameraShareRoot -Directory | ForEach-Object {
# Get the most recently created file in each folder
$_ | Get-ChildItem -Recurse -Filter '*.mkv' -File | Sort-Object -Property CreationTime -Descending | Select-Object -First 1
} | Where-Object {
# Remove any files that were created after our datetime
$_.CreationTime -lt $MinFileAge;
} | Select-Object -Property FullName, CreationTime
This will just output the full file name and creation time for stale cameras.
You could do something like this to email yourself a report when the results have any files:
# The path to your files
$CameraShareRoot = 'D:\Shares\Cameras';
# Number of Hours
$NumberOfHours = 1;
# Date and time of significance. It's $NumberOfHours in the past.
$MinFileAge = (Get-Date).AddHours( - $NumberOfHours);
# Get all the folders at the camera share root, save the results to $StaleCameraFiles
$StaleCameraFiles = Get-ChildItem -Path $CameraShareRoot -Directory | ForEach-Object {
# Get the most recently created file in each folder
$_ | Get-ChildItem -Recurse -Filter '*.mkv' -File | Sort-Object -Property CreationTime -Descending | Select-Object -First 1;
} | Where-Object {
# Remove any files that were created after our datetime
$_.CreationTime -lt $MinFileAge;
}
# If there are any stale camera files
if ($StaleCameraFiles) {
# Send an email
$MailMessage = #{
SmtpServer = 'mail.example.com';
To = 'youremail#example.com';
From = 'youremail#example.com';
Subject = 'Stale Camera Files';
Body = $StaleCameraFiles | Select-Object -Property FullName, CreationTime | ConvertTo-Html -Fragment | Out-String;
BodyAsHtml = $true;
}
Send-MailMessage #MailMessage;
}
Generally you will want to use LastWriteTime instead of CreationTime since the latter can be updated by a file move or copy, but maybe that's what you want here.
You have to compare the CreationTime date with (Get-Date).AddHours(-1). The AddHours method allows you to add hours to the DateTime, but also to subtract.
You can use the following example:
$Path = 'd:\Shares\Cameras'
$CreationTime = Get-ChildItem -Path $Path -Filter *.mkv |
Sort-Object -Property CreationTime -Descending |
Select-Object -First 1 -ExpandProperty CreationTime
if ($CreationTime -lt (Get-Date).AddHours(-1)) {
# your action here (restart, send mail, write output, ...)
}
It also optimizes your code a bit. ;)
$LatestFile = Get-ChildItem C:\Users\Connor\Desktop\ | Sort CreationTime | Select -Last 1
if ($LatestFile.CreationTime -gt (Get-Date).AddHours(-1)){
#It's Currently Working
} else {
#Do Other Stuff
}
try this :
Get-ChildItem "c:\temp" -Filter *.mkv -File | sort CreationTime -Descending |
select -First 1 | where CreationTime -lt (Get-Date).AddHours(-1) |
%{Write-Host "Alert !!" -ForegroundColor Red}
I want the greatest value (mailboxSize) at the top of the file. I have a cvs as inport.
When I do the following sort cmd:
Import-Csv import.csv| Sort-Object MailboxSize,DisplayName -Descending | Export-Csv SORT.csv
I get the following result:
"DisplayName","MailboxSize"
"persone6","9941"
"persone3","8484"
"persone1","7008"
"persone4","4322"
"persone5","3106"
"persone7","27536"
"persone10","24253"
"persone8","1961"
"persone9","17076"
"persone11","17012"
"persone2","15351"
"persone12","11795"
"persone14","1156"
"persone13","1008"
But I want this as a result!
"persone7","27536"
"persone10","24253"
"persone9","17076"
"persone11","17012"
"persone2","15351"
"persone12","11795"
"persone6","9941"
"persone3","8484"
"persone1","7008"
"persone4","4322"
"persone5","3106"
"persone14","1156"
"persone13","1008"
When importing a CSV-file, all properties are made string-type. You have to cast the MailboxSize to an int before you can sort it properly. Try:
Import-Csv import.csv |
Sort-Object {[int]$_.MailboxSize}, DisplayName -Descending |
Export-Csv SORT.csv
You should also use the -NoTypeInformation switch in Export-CSV to avoid the #TYPE ..... line (first line in an exported CSV-file).
Sample:
$data = #"
"DisplayName","MailboxSize"
"persone6","9941"
"persone3","8484"
"persone1","7008"
"persone4","4322"
"persone5","3106"
"persone7","27536"
"persone10","24253"
"persone8","1961"
"persone9","17076"
"persone11","17012"
"persone2","15351"
"persone12","11795"
"persone14","1156"
"persone13","1008"
"# | ConvertFrom-Csv
$data |
Sort-Object {[int]$_.MailboxSize}, DisplayName -Descending |
Export-Csv SORT.csv -NoTypeInformation
SORT.csv
"DisplayName","MailboxSize"
"persone7","27536"
"persone10","24253"
"persone9","17076"
"persone11","17012"
"persone2","15351"
"persone12","11795"
"persone6","9941"
"persone3","8484"
"persone1","7008"
"persone4","4322"
"persone5","3106"
"persone8","1961"
"persone14","1156"
"persone13","1008"
I'm guessing the usernames are fake, but be aware that the same issue goes for DisplayName if your usernames actually was personeXX where XX is an int. Like:
persone7 27536
persone20 27536
persone13 27536
To sort them probably, you'd have to create a scriptblock for Sort-Object or create your own function to split the value and sort them correctly.
I have a file containing a list of email addresses:
e.g.
personB#placeB.com
nameA#location2.com
nameB#location2.com
I want to order them, but by the domain, and then by the local, and then output the ordered list. so I end up with:
nameA#location2.com
nameB#location2.com
personB#placeB.com
I can use sort-object to order them, but this is by the whole string.
So far I have got this far:
SELECT-STRING -path path_To_My_File |
FOREACH{
$a = $_.split"(#)"
$local = $a[0]
$domain = $a[1]
}
but can't see how to incorporate the sort-object.
Similar to Matt's answer, but will give you the sorted email addresses as a list:
$addresses = #()
Get-Content .\path\to\file | % {
$n = $($_ -split '#')[0]
$d = $($_ -split '#')[1]
$o = New-Object PSObject -property #{name=$n;domain=$d;email=$_}
$addresses += ,$o
}
$addresses | sort domain,name | select -ExpandProperty email
The $addresses array can be sorted by the object you created on the fly in the Foreach-Object or % loop, and then you can just select the email property to give you the sorted list
Sort-Object obviously needs an object to sort on. So we can create an object like this. ( There are other ways as well). Also we dont need to use select-string since you are applying no patterns
Get-Content -path "path_To_My_File" |
Select-Object #{Name="Local";Expression={($_.split("#"))[0]}},#{Name="Domain";Expression={($_.split("#"))[1]}} |
Sort-Object domain,local
Create properties using Select-Object for Local and Domain using the same split that you had. The select on its own would output this:
Local Domain
----- ------
personB placeB.com
nameA location2.com
nameB location2.com
Then we can sort to get this:
Local Domain
----- ------
nameA location2.com
nameB location2.com
personB placeB.com
If you need the full address as well we can shorten the code and add that variable.
Get-Content -path "path_To_My_File" |
Select #{N="Address";E={$_}},
#{N="Local";E={($_.split("#"))[0]}},
#{N="Domain";E={($_.split("#"))[1]}} |
Sort domain,local
Are there any cool cmdlets that will help me do the following?
I want something in Powershell that is as simple as doing the same in SQL:
select RootElementName , count(*) from Table
group by RootElementName
order by RootElementName
I'm all XML files in a directory, finding the root element of each XML file.
$DirectoryName = "d:\MyFolder\"
$AllFiles = Get-ChildItem $DirectoryName -Force
foreach ($Filename in $AllFiles)
{
$FQFilename = $DirectoryName + $Filename
[xml]$xmlDoc = Get-Content $FQFilename
$rootElementName = $xmlDoc.SelectSingleNode("/*").Name;
Write-Host $FQFilename $rootElementName
}
Desired Result:
RootName Count
-------- -----
Root1 15
MyRoot 16
SomeRoot 24
I know I could could either create two arrays, or an array of objects, store the root elements in the array, and do the counts all using typical code, was just hoping that this new language might have something built-in that I haven't discovered yet.
Could I pipe the "Write-Host $FQFilename $rootElementName " to something that would behave something to the SQL I referred to above?
You can get groups and counts by using Group-Object like this:
$AllFiles | Group-Object RootElementName | Sort-Object Name | Select-Object Name, Count
In your current example, Write-Host doesn't write an object to the pipeline that we can sort or group. Write-Host only prints text to the screen to show the user something, ex. a script menu.
$DirectoryName = "d:\MyFolder\"
$AllFiles = Get-ChildItem $DirectoryName -Force | ForEach-Object {
#The FullName-property contains the absolute path, so there's no need to join the filename and $directoryname
[xml]$xmlDoc = Get-Content $_.FullName
$rootElementName = $xmlDoc.SelectSingleNode("/*").Name
#Outputing an object that we can group and sort
New-Object -TypeName psobject -Property #{
FileName = $_.FullName
RootElementName = $rootElementName
}
}
$grped = $AllFiles | Group-Object RootElementName | Sort-Object Name | Select-Object Name, Count
I'm creating an object with a FileName-property and the RootElementName so you have it if you need to retrieve the filename+rootelement for a list. If not, we could simplify this to:
$DirectoryName = "d:\MyFolder\"
$AllFiles = Get-ChildItem $DirectoryName -Force | ForEach-Object {
#The FullName-property contains the absolute path, so there's no need to join the filename and $directoryname
[xml]$xmlDoc = Get-Content $_.FullName
#Output rootelementname
$xmlDoc.SelectSingleNode("/*").Name
}
$grped = $AllFiles | Group-Object | Sort-Object Name | Select-Object Name, Count