In PowerShell, if I have a list of strings containing versions, "3.0.1.1", "3.2.1.1", etc., how can I sort it the way System.Version would sort it in C#?
PS C:\> $ver="3.0.1.1","3.2.1.1"
PS C:\> $ver|%{[System.Version]$_}|sort
Major Minor Build Revision
----- ----- ----- --------
3 0 1 1
3 2 1 1
Just convert it to a Version and sort that way:
$list = "3.0.1.1","3.2.1.1"
$sorted = $list | %{ new-object System.Version ($_) } | sort
A version string can be cast to a Version object, and
sort-object can be passed a script block and sort on the result.
PS C:\Users\me> "3.11.0.1", "3.0.1.1", "3.2.1.1" | sort
3.0.1.1
3.11.0.1
3.2.1.1
PS C:\Users\me> "3.11.0.1", "3.0.1.1", "3.2.1.1" | sort {[version] $_}
3.0.1.1
3.2.1.1
3.11.0.1
(Added an extra version string to make the example actually meaningful.)
# I needed to sort historical versions (Octopus) with varying decimal formats.
# Try # this (it is easy to add to a more complex expression sort)
# Special Case "3.00.1.10.1.10" and "3.0.1.10.1.10" required the double sort
# to work correctly
$vers = #()`enter code here`
$vers += #( "3.1.60", "3.1.52","3.1.51")
$vers += #( "3.00.46", "3.00.36","3.50.2145.11")
$vers += #( "3.50.2145.10","3.50.2145.9")
$vers += #( "3.50.2145.8", "3.50.2145.7")
$vers += #( "3.50.2145.6", "3.50.2145.5")
$vers += #( "3.50.2145.4", "3.50.2145.3")
$vers += #( "3.50.2145.2", "3.50.2145.1")
$vers += #( "3.50.2145", "3.50.2143")
$vers += #( "3.50.2141", "3.50.2135")
$vers += #( "3.0.1.10.1.1", "3.00.1.10.1.10")
$vers += #( "2.1.3.4", "3.0","3.")
$vers += #( "3.0.1.10.1.100","3.0.1.10.1.10")
$mySortAsc = #{Expression={ [regex]::Replace($_ ,'\d+', { $args[0].Value.PadLeft(20,'0') }) };Descending=$false}
$mySortDesc = #{Expression={ [regex]::Replace($_ ,'\d+', { $args[0].Value.PadLeft(20,'0') }) };Descending=$true}
$nl = [Environment]::NewLine
Write-Output ($nl + "Ascending Sort" + $nl);
$vers | Sort-Object | Sort-Object $mySortAsc
Write-Output ($nl + "Descending Sort" + $nl);
$vers | Sort-Object -Descending | Sort-Object $mySortDesc
<# Result
Ascending Sort
2.1.3.4
3.
3.0
3.0.1.10.1.1
3.0.1.10.1.10
3.00.1.10.1.10
3.0.1.10.1.100
3.00.36
3.00.46
3.1.51
3.1.52
3.1.60
3.50.2135
3.50.2141
3.50.2143
3.50.2145
3.50.2145.1
3.50.2145.2
3.50.2145.3
3.50.2145.4
3.50.2145.5
3.50.2145.6
3.50.2145.7
3.50.2145.8
3.50.2145.9
3.50.2145.10
3.50.2145.11
Descending Sort
3.50.2145.11
3.50.2145.10
3.50.2145.9
3.50.2145.8
3.50.2145.7
3.50.2145.6
3.50.2145.5
3.50.2145.4
3.50.2145.3
3.50.2145.2
3.50.2145.1
3.50.2145
3.50.2143
3.50.2141
3.50.2135
3.1.60
3.1.52
3.1.51
3.00.46
3.00.36
3.0.1.10.1.100
3.00.1.10.1.10
3.0.1.10.1.10
3.0.1.10.1.1
3.0
3.
2.1.3.4
#>
Just to add another corner case: powershell treats this single digit kind of version '2' as invalid.
Have to add '.0' to the end to create the version object before sorting:
if($version -match '^\d$')
{
$version = $version + '.0'
}
New-Object System.Version $version
Related
I have an array of objects:
$people= #()
foreach ($person in $databaseOfGuests)
{
$people += #{ "FirstName"=$person.FirstName; "LastName"=$person.LastName }
}
Now I want to remove duplicates from $people, is it possible to do this in PowerShell? Something like:
$people = $people | Select -Uniq
I have two arrays $people1 and $people2, I need to get array of all people that are in $people1 but not in the $people2 and vise versa. Something like:
$peopleInPeople1ButNotInPeople2 = $people1.Substruct($people2)
$peopleInPeople2ButNotInPeople1 = $people2.Substruct($people1)
Is it possible to do it in one line in PS?
Try this:
$people1 | ? {$_ -notin $people2}
or you can filter by property, like firstname or lastname:
$people1 | ? {$_.Firstname -notin $people2.Firstname}
the -notin operator is available on PS3 and above, for lower versions you can use -notcontains
My first thought was to fall back to the .NET Framework and use LINQ. But it turns out that the PowerShell syntax is pretty ugly:
[Linq.Enumerable]::Except([object[]] $people1, [object[]] $people2)
(I'm not sure why the object[] casts are necessary, but without them PowerShell throws an "Cannot find an overload for "Except" and the argument count: "2"" exception.)
The idiomatic PowerShell would be:
$people1 | Where-Object { $people2 -notcontains $_ }
In addition to other good answers, following code depicts all SET operation on PowerShell Arrays:
#Union - Subtraction - Minus:
$a = (1,2,3,4)
$b = (1,2,77,88)
'UNION:'
Compare-Object $a $b -PassThru -IncludeEqual # union
''
'INTERSECTION:'
Compare-Object $a $b -PassThru -IncludeEqual -ExcludeDifferent # intersection
''
'MINUS:'
'$a minus $b:'
$a | ?{$b -notcontains $_} # a -minus b
'$b minus $a:'
$b | ?{$a -notcontains $_} # b -minus a
HTH.
One of the things I like to do in Powershell is:
Set-Location 'C:\Windows\System32\WindowsPowerShell\v1.0\en-US\'
Get-ChildItem *.txt | Format-Wide -Column 3
This gives me a great view on everything there is to learn and explore. The thing that bothered me is the sorting, because now I have 3 columns that start with 'A'. It would be more readable when I'd have (for example) one column with A-J, one column L-R, and one going from R-Z. This bothered me so much, I wrote a function to do it:
Function PSHELP {
Set-Location 'C:\Windows\System32\WindowsPowerShell\v1.0\en-US\'
#Initialize variables
$files = gci *.txt | select name
$count = $files.count
$remainder = $count % 3
$rows = ($count - $remainder) /3 -as [int]
#I add an extra row by default, to deal with remainders
$rows++
$Table = New-Object 'object[,]' $rows,3
#Build up a table the way I want to see it
#column 1: A,B,C...
#column 2: L,M,N..
#column 3: R,...,Z
$index = 0
for ($j = 0; $j -lt 3; $j++)
{
$ThisColumnLength = $rows
if($j -ge $remainder){
$ThisColumnLength--
}
for ($i = 0; $i -lt $ThisColumnLength; $i++)
{
$table[$i,$j] = $files[$index]
$index++
}
}
#Read the table in the order Format-Wide throws them on the screen
#And store this in an array
$array = #()
for ($i = 0; $i -lt $rows; $i++)
{ $ThisRowLength = 3
if(($i+1) -eq $Rows){
$ThisRowLength = $remainder
}
if ($ThisRowLength -gt 0){
for ($j = 0; $j -lt $ThisRowLength; $j++)
{
$array += $table[$i,$j]
}
}
}
$array | fw -Column 3
}
Is there a more 'standard' way to do this in powershell? It seems like quite a natural option to me, but I couldn't find it. Is there an option or command that I've missed?
To clarify: I am not looking for ways to find help. This question is about the Format-Wide command, and/or possible alternative. I just thought this would be a nice example.
[Edit:] Changed my function to something slightly less clumbsy.
[Edit2:] The code I posted is flawed, and it's getting late. If you paste it in the shell and compare it with {Get-Childitem *.txt | format-wide -column 3}', you should be able to see what I am trying to do here. I hope somebody can suggest some kind of alternative.
[Edit3:] Modified the code again, finally got this to work. In the process I found out a very interesting thing about what Format-Wide returns:
PS> (Get-ChildItem).count
Result: 125
PS> (Get-ChildItem | Format-Wide).count
Result: 129
This confused me a lot, because sometimes I counted the results and didn't get what I expected, so a couple of times I thought something was wrong with my code, but maybe everything was fine.
I found something that does exactly what I want, and is more generic:
http://www.leporelo.eu/blog.aspx?id=powershell-formatting-format-wide-rotated-to-format-columns (dead link)
Version 0.4 code (pasted at the bottom of this answer in case this link dies too)
Archived 0.1 code (included because it has more discussion/examples)
Example with regular Format-Wide
'The basic promise behind implicit remoting is that you can work ' +
'with remote commands using local syntax.' -split ' ' | Format-Wide -force -prop {$_} -col 3
The basic promise
behind implicit remoting
is that you
can work with
remote commands using
local syntax.
Example with Format-Columns
'The basic promise behind implicit remoting is that you can work ' +
'with remote commands using local syntax.' -split ' ' | . Format-Columns -col 3
The is remote
basic that commands
promise you using
behind can local
implicit work syntax.
remoting with
I am still surprised Powershell doesn't have a built-in option to do this.
Version 0.4 code:
function Format-Columns {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[PsObject[]]$InputObject,
[Object]$Property,
[int]$Column,
[int]$MaxColumn,
[switch]$Autosize
)
Begin { $values = #() }
Process { $values += $InputObject }
End {
function ProcessValues {
$ret = $values
$p = $Property
if ($p -is [Hashtable]) {
$exp = $p.Expression
if ($exp) {
if ($exp -is [string]) { $ret = $ret | % { $_.($exp) } }
elseif ($exp -is [scriptblock]) { $ret = $ret | % { & $exp $_} }
else { throw 'Invalid Expression value' }
}
if ($p.FormatString) {
if ($p.FormatString -is [string]) { $ret = $ret | % { $p.FormatString -f $_ } }
else { throw 'Invalid format string' }
}
}
elseif ($p -is [scriptblock]) { $ret = $ret | % { & $p $_} }
elseif ($p -is [string]) { $ret = $ret | % { $_.$p } }
elseif ($p -ne $null) { throw 'Invalid -property type' }
# in case there were some numbers, objects, etc., convert them to string
$ret | % { $_.ToString() }
}
if (!$Column) { $Autosize = $true }
$values = ProcessValues
$valuesCount = #($values).Count
if ($valuesCount -eq 1) {
return $values
}
# from some reason the console host doesn't use the last column and writes to new line
$consoleWidth = $host.ui.RawUI.maxWindowSize.Width - 1
$gutterWidth = 2
# get length of the longest string
$values | % -Begin { [int]$maxLength = -1 } -Process { $maxLength = [Math]::Max($maxLength,$_.Length) }
# get count of columns if not provided
if ($Autosize) {
$Column = [Math]::Max( 1, ([Math]::Floor(($consoleWidth/($maxLength+$gutterWidth)))) )
$remainingSpace = $consoleWidth - $Column*($maxLength+$gutterWidth);
if ($remainingSpace -ge $maxLength) {
$Column++
}
if ($MaxColumn -and $MaxColumn -lt $Column) {
$Column = $MaxColumn
}
}
$countOfRows = [Math]::Ceiling($valuesCount / $Column)
$maxPossibleLength = [Math]::Floor( ($consoleWidth / $Column) )
# cut too long values, considers count of columns and space between them
$values = $values | % {
if ($_.length -gt $maxPossibleLength) { $_.Remove($maxPossibleLength-2) + '..' }
else { $_ }
}
#add empty values so that the values fill rectangle (2 dim array) without space
if ($Column -gt 1) {
$values += (#('') * ($countOfRows*$Column - $valuesCount))
}
# in case there is only one item, make it array
$values = #($values)
<#
now we have values like this: 1, 2, 3, 4, 5, 6, 7, ''
and we want to display them like this:
1 3 5 7
2 4 6 ''
#>
$formatString = (1..$Column | %{ "{$($_-1),-$maxPossibleLength}" }) -join ''
1..$countOfRows | % {
$r = $_-1
$line = #(1..$Column | %{ $values[$r + ($_-1)*$countOfRows]} )
Write-Output "$($formatString -f $line)".PadRight($consoleWidth,' ')
}
}
<#
.SYNOPSIS
Formats incoming data to columns.
.DESCRIPTION
It works similarly as Format-Wide but it works vertically. Format-Wide outputs the data row by row, but Format-Columns outputs them column by column.
.PARAMETER Property
Name of property to get from the object.
It may be
-- string - name of property.
-- scriptblock
-- hashtable with keys 'Expression' (value is string=property name or scriptblock)
and 'FormatString' (used in -f operator)
.PARAMETER Column
Count of columns
.PARAMETER Autosize
Determines if count of columns is computed automatically.
.PARAMETER MaxColumn
Maximal count of columns if Autosize is specified
.PARAMETER InputObject
Data to display
.EXAMPLE
1..150 | Format-Columns -Autosize
.EXAMPLE
Format-Columns -Col 3 -Input 1..130
.EXAMPLE
Get-Process | Format-Columns -prop #{Expression='Handles'; FormatString='{0:00000}'} -auto
.EXAMPLE
Get-Process | Format-Columns -prop {$_.Handles} -auto
.NOTES
Name: Get-Columns
Author: stej, http://twitter.com/stejcz
Site: http://www.leporelo.eu/blog.aspx?id=powershell-formatting-format-wide-rotated-to-format-columns
Lastedit: 2017-09-11
Version 0.4 - 2017-09-11
- removed color support and changed output from Write-Host to Write-Output
Version 0.3 - 2017-04-24
- added ForegroundColor and BackgroundColor
Version 0.2 - 2010-01-14
- added MaxColumn
- fixed bug - displaying collection of 1 item was incorrect
Version 0.1 - 2010-01-06
#>
}
If you mean standard way of finding all the help files in PowerShell, then yes there:
Get-Help * -Category HelpFile
Outside of that I just go check this page on Technet: Windows PowerShell Core About Topics
Here is a fork of the above script in a module form: https://github.com/loxia01/FormatColumn
Function syntax:
Format-Column [[-Property] <Object>] [-MaxColumnCount <int>] [-MinRowCount <int>] [-OrderBy <string>] [-InputObject <psobject>] [<CommonParameters>]
Format-Column [[-Property] <Object>] -ColumnCount <int> [-OrderBy <string>] [-InputObject <psobject>] [<CommonParameters>]
I am having issues finding the correct syntax I need to filter my results on only listing directories with a file count of above a specified amount (600 in my case).
This is my code so far;
$server_dir= "D:\backup"
$export_dir= "C:\support\spcount.txt"
if($server_dir)
{
$folders = Get-ChildItem $server_dir
$output = #()
foreach($folder in $folders)
{
$fname = $folder.Name
$fpath = $folder.FullName
$fcount = Get-ChildItem $fpath | Measure-Object | Select-Object -Expand Count
$obj = New-Object psobject -Property #{FolderName = $fname; FileCount = $fcount} | Format-List;
$output += $obj
}
#Output
$output | Tee-Object -FilePath $export_dir | Format-list FileCount
}
And I am getting positive results with this, it is listing all Child Items within the backup dir however I need to filter this to only display and out too text format IF the directory contains 600 or more files.
Can anybody help me please?
I am fairly new too powershell so please pull me up if this code is not the greatest, I am forever wanting too learn.
Thanks!
I think I found the issue. It's that Format-List statement at the end of your object creation statement. It pipes the newly created object through Format-List, and thus transforms it into something else.
$obj = New-Object psobject -Property #{FolderName = $fname; FileCount = $fcount} | Format-List
So if you remove that last bit, you'll get the object you expect
$obj = New-Object psobject -Property #{FolderName = $fname; FileCount = $fcount}
So when you use the where statement to filter, you'll actually have a FileCount property to filter on.
I detected it by running the $output through Get-Member which showed me it wasn't the object with the expected properties.
So basically, here's your code, including fixes:
if($server_dir)
{
# *** Added the -directory flag, cause we don't need those pesky files ***
$folders = Get-ChildItem $server_dir -directory
$output = #()
foreach($folder in $folders)
{
$fname = $folder.Name
$fpath = $folder.FullName
$fcount = Get-ChildItem $fpath | Measure-Object | Select-Object -Expand Count
# *** Format-List was dropped here to avoid losing the objects ***
$obj = New-Object psobject -Property #{FolderName = $fname; FileCount = $fcount}
$output += $obj
}
# *** And now the filter and we're done ***
$output | where -Property FileCount -ge 600 | Tee-Object -FilePath $export_dir | Format-list FileCount
}
Note also the -directory to get only folders with get-childitem, and the -ge 600 (greater than or equal) instead of -gt 599 which is just a bit more obvious.
Remember that the Format-* statements actually transform the data passed through them. So you should only use those at the end of the pipeline to show data on screen or dump it to a file.
Don't use it to transform the data you still want to work with later on.
So in short you could do something like this to get that information.
Get-ChildItem C:\temp -Directory |
Select Name,#{Label="Count";Expression={(Get-Childitem $_ -file -Recurse).Count}} |
Where-Object{$_.Count -lt 10}
Let see if we can incorporate that in your code. Your if statement is also kind of pointless. Your variable contains a non-null \ non-zerolength string so it will always be True. You want it to work if the directory exists I imagine.
$server_dir= "D:\backup"
$export_dir= "C:\support\spcount.txt"
if(Test-Path $server_dir){
Get-ChildItem C:\temp -Directory |
Select Name,#{Label="Count";Expression={(Get-Childitem $_ -file -Recurse).Count}} |
Where-Object{$_.Count -lt 10} |
ConvertTo-Csv | Tee -File $export_dir | ConvertFrom-Csv
} Else {
Write-Warning "$server_dir does not exist."
}
Just working on getting this to file and screen with Tee just a moment.
I see 2 ways to do this.
Filter it in your output like this:
$output | where -property FileCount -gt 599 | # ... your code to write to the output
Or not store it in the output array if it doesn't match the condition:
if ($fcount -gt 599) {
$obj = New-Object psobject -Property #{FolderName = $fname; FileCount = $fcount} | Format-List;
$output += obj
}
i'm new to powershell so any help is much appreciated.
I have an list of strings (filenames) and they start with a number eg. "1. first file", "2. second file" ... "21. twenty-first file".
and I want to sort it in the order of the starting number. But when I do "$List | sort {$_.Name} -Unique" it will start with "1. " and then the next item is "11." instead of "2."
Please help.
Here is one way to do it:
$test = #('1. First', '2. Second', '11. Eleventh')
$sort = #()
foreach($item in $test){
$item -match '^(\d+).*'
$temp = New-Object PSCustomObject -property #{'Number' = [int]$matches[1]}
$sort += $temp
}
$sort | Sort-Object Number | Select-Object Data
Here's another option:
$col = #(
'1. first file'
'2. second file'
'11. eleventh file'
'21. twenty-first file'
)
$ht = #{}
$col | foreach {$ht[$_ -replace '\D+$'] = $_}
[int[]]$ht.keys | sort | foreach {$ht["$_"] }
1. first file
2. second file
11. eleventh file
21. twenty-first file
To simplify the previous answer a little, you could do this:
$test | Sort-Object -Property #{ Expression={[int]$_.substring(0,$_.IndexOf('.'))}; Ascending=$true }
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