TheMadTechnician's answer works as long as the range start addresses differ in the first octet. To get it to sort by multiple octets, it doesn't look like Sort-Object
will sort by successive values in an array returned by a single [ScriptBlock]
; for that you'd need to pass a [ScriptBlock]
for each octet. Santiago Squarzon's answer shows how to do that without the repetition of defining four almost-identical [ScriptBlock]
s.
Instead, a single [ScriptBlock]
can combine each octet into a [UInt32]
on which to sort.
Using [Math]::Pow()
to produce a sortable value
Get-Content -Path 'IPv4AddressRanges.txt' |
Sort-Object -Property {
# Split each line on a hyphen surrounded by optional whitespace
$rangeStartAddress = ($_ -split '\s*-\s*')[0]
# Split the start address on a period and parse the resulting [String]s to [Byte]s
[Byte[]] $octets = $rangeStartAddress -split '.', 0, 'SimpleMatch'
#TODO: Handle $octets.Length -ne 4
# Alternative: [Byte[]] $octets = [IPAddress]::Parse($rangeStartAddress).GetAddressBytes()
[UInt32] $sortValue = 0
# $sortValue = (256 ^ 3) * $octets[0] + (256 ^ 2) * $octets[1] + 256 * $octets[2] + $octets[3]
for ($i = 0; $i -lt $octets.Length; $i++)
{
$octetScale = [Math]::Pow(256, $octets.Length - $i - 1)
$sortValue += $octetScale * $octets[$i]
}
return $sortValue
}
...which outputs...
62.193.0.0 - 62.193.31.255
78.39.193.192 - 78.39.193.207
78.39.194.0 - 78.39.194.255
85.185.240.128 - 85.185.240.159
194.225.0.0 - 194.225.15.255
194.225.24.0 - 194.225.31.255
195.146.40.0 - 195.146.40.255
195.146.53.128 - 195.146.53.225
217.218.0.0 - 217.219.255.255
For good measure you can change the first line to...
@('255.255.255.255', '0.0.0.0') + (Get-Content -Path 'IPv4AddressRanges.txt') |
...and see that it sorts correctly without the sort value overflowing.
Using [BitConverter]
to produce a sortable value
You can simplify the above by using the [BitConverter]
class to convert the IP address bytes directly to a [UInt32]
...
Get-Content -Path 'IPv4AddressRanges.txt' |
Sort-Object -Property {
# Split each line on a hyphen surrounded by optional whitespace
$rangeStartAddress = ($_ -split '\s*-\s*')[0]
# Split the start address on a period and parse the resulting [String]s to [Byte]s
[Byte[]] $octets = $rangeStartAddress -split '.', 0, 'SimpleMatch'
#TODO: Handle $octets.Length -ne 4
# Alternative: [Byte[]] $octets = [IPAddress]::Parse($rangeStartAddress).GetAddressBytes()
# [IPAddress]::NetworkToHostOrder() doesn't have an overload for [UInt32]
if ([BitConverter]::IsLittleEndian)
{
[Array]::Reverse($octets)
}
return [BitConverter]::ToUInt32($octets, 0)
}
Implementing [IComparable]
in a PowerShell class to define its own sorting
A more sophisticated solution would be to store our addresses in a type implementing the [IComparable]
interface so Sort-Object
can sort the addresses directly without needing to specify a [ScriptBlock]
. [IPAddress]
is, of course, the most natural .NET type in which to store an IP address, but it doesn't implement any sorting interfaces. Instead, we can use PowerShell classes to implement our own sortable type...
# Implement System.IComparable[Object] instead of System.IComparable[IPAddressRange]
# because PowerShell does not allow self-referential base type specifications.
# Sort-Object seems to only use the non-generic interface, anyways.
class IPAddressRange : Object, System.IComparable, System.IComparable[Object]
{
[IPAddress] $StartAddress
[IPAddress] $EndAddress
IPAddressRange([IPAddress] $startAddress, [IPAddress] $endAddress)
{
#TODO: Ensure $startAddress and $endAddress are non-$null
#TODO: Ensure the AddressFamily property of both $startAddress and
# $endAddress is [System.Net.Sockets.AddressFamily]::InterNetwork
#TODO: Handle $startAddress -gt $endAddress
$this.StartAddress = $startAddress
$this.EndAddress = $endAddress
}
[Int32] CompareTo([Object] $other)
{
if ($null -eq $other)
{
return 1
}
if ($other -isnot [IPAddressRange])
{
throw [System.ArgumentOutOfRangeException]::new(
'other',
"Comparison against type ""$($other.GetType().FullName)"" is not supported."
)
}
$result = [IPAddressRange]::CompareAddresses($this.StartAddress, $other.StartAddress)
if ($result -eq 0)
{
$result = [IPAddressRange]::CompareAddresses($this.EndAddress, $other.EndAddress)
}
return $result
}
hidden static [Int32] CompareAddresses([IPAddress] $x, [IPAddress] $y)
{
$xBytes = $x.GetAddressBytes()
$yBytes = $y.GetAddressBytes()
for ($i = 0; $i -lt 4; $i++)
{
$result = $xBytes[$i].CompareTo($yBytes[$i])
if ($result -ne 0)
{
return $result
}
}
return 0
}
}
The [IPAddressRange]
type stores both the start and end address of a range, so it can represent an entire line of your input file. The CompareTo
method compares each StartAddress
byte-by-byte and only if those are equal does it then compare each EndAddress
byte-by-byte. Executing this...
(
'127.0.0.101 - 127.0.0.199',
'127.0.0.200 - 127.0.0.200',
'127.0.0.100 - 127.0.0.200',
'127.0.0.100 - 127.0.0.101',
'127.0.0.199 - 127.0.0.200',
'127.0.0.100 - 127.0.0.199',
'127.0.0.100 - 127.0.0.100',
'127.0.0.101 - 127.0.0.200'
) + (Get-Content -Path 'IPv4AddressRanges.txt') |
ForEach-Object -Process {
$startAddress, $endAddress = [IPAddress[]] ($_ -split '\s*-\s*')
return [IPAddressRange]::new($startAddress, $endAddress)
} |
Sort-Object
...sorts the 127.0.0.*
ranges in the expected order...
StartAddress EndAddress
------------ ----------
62.193.0.0 62.193.31.255
78.39.193.192 78.39.193.207
78.39.194.0 78.39.194.255
85.185.240.128 85.185.240.159
127.0.0.100 127.0.0.100
127.0.0.100 127.0.0.101
127.0.0.100 127.0.0.199
127.0.0.100 127.0.0.200
127.0.0.101 127.0.0.199
127.0.0.101 127.0.0.200
127.0.0.199 127.0.0.200
127.0.0.200 127.0.0.200
194.225.0.0 194.225.15.255
194.225.24.0 194.225.31.255
195.146.40.0 195.146.40.255
195.146.53.128 195.146.53.225
217.218.0.0 217.219.255.255
Note that we've only added the ability for Sort-Object
to sort [IPAddressRange]
instances and not its individual properties. Those are still of type [IPAddress]
which doesn't provide its own ordering, so if we try something like ... | Sort-Object -Property 'EndAddress'
it will not produce the desired results.