6

I have a plain text file containing some IPs like this:

194.225.0.0 - 194.225.15.255
194.225.24.0 - 194.225.31.255
62.193.0.0 - 62.193.31.255
195.146.53.128 - 195.146.53.225
217.218.0.0 - 217.219.255.255
195.146.40.0 - 195.146.40.255
85.185.240.128 - 85.185.240.159
78.39.194.0 - 78.39.194.255
78.39.193.192 - 78.39.193.207

I want to sort file by IP Addresses. I mean only the first part is important.

I googled and found some programs but I want to know whether that's possible via Powershell with no other applications.

I have a Linux way like this but was unable to reach it in Windows:

sort -n -t . -k 1,1 -k 2,2 -k 3,3 -k 4,4 file

Update1

@TheMadTechnician, this is the output when I run your command:

85.185.240.128 - 85.185.240.159
195.146.40.0 - 195.146.40.255
78.39.193.192 - 78.39.193.207
78.39.194.0 - 78.39.194.255
217.218.0.0 - 217.219.255.255
194.225.24.0 - 194.225.31.255
194.225.0.0 - 194.225.15.255
195.146.53.128 - 195.146.53.225
62.193.0.0 - 62.193.31.255
Lance U. Matthews
  • 15,725
  • 6
  • 48
  • 68
Saeed
  • 3,255
  • 4
  • 17
  • 36

8 Answers8

9

A simple solution using RegEx-replace: To make IP addresses sortable, we just need to pad each octet on the left side so they all have the same width. Then a simple string comparison yields the correct result.

For PS 6+:

Get-Content IpList.txt | Sort-Object {
    $_ -replace '\d+', { $_.Value.PadLeft(3, '0') }
}

For PS 5.x:

Get-Content IpList.txt | Sort-Object {
    [regex]::Replace( $_, '\d+', { $args.Value.PadLeft(3, '0') } )
}
  • The -replace operator tries to find matches of a regular expression pattern within a given string and replaces them by given values.
  • For PS 5.x we need a different syntax, because -replace doesn't support a scriptblock. Using the .NET Regex.Replace method we can achieve the same.
  • The first $_ denotes the current line of the text file.
  • \d+ is a pattern that matches each octet of each IP address. For detailed explanation see example at regex101.
  • {} defines a scriptblock that outputs the replacement value
    • Here $_ denotes the current match (octet). We take its value and fill it with zeros on the left side, so each octet will be 3 characters in total (e. g. 2 becomes 002 and 92 becomes 092). A final IP may look like 194.225.024.000.

Another solution using the Tuple class. It is slightly longer, but cleaner because it actually compares numbers instead of strings.

Get-Content IpList.txt | Sort-Object {
    # Extract the first 4 numbers from the current line
    [int[]] $octets = [regex]::Matches( $_, '\d+' )[ 0..3 ].Value
    
    # Create and output a tuple that consists of the numbers
    [Tuple]::Create( $octets[0], $octets[1], $octets[2], $octets[3] )  
}
  • Using [regex]::Matches() we find all numbers of the current line. From the returned MatchCollection we take the first four elements. Then we use member access enumeration to create a string array of the Value member of each MatchCollection element.

  • By simply assigning the string array to a variable with the [int[]] type constraint (array of ints), PowerShell automatically parses the strings as integers.

  • The sorting works because Tuple implements the IComparable interface, which Sort-Object uses when available. Tuples are sorted lexicographically, as expected.

  • Using dynamic method invocation, we may shorten the [Tuple]::Create call like this (which works for up to 8 elements1):

     [Tuple]::Create.Invoke( [object[]] $octets )
    

    Note the conversion to [object[]], otherwise [Tuple]::Create would be called with only a single argument, that is the $octets array.


[1] Actually tuples bigger than 8 elements can be created by creating nested tuples (create a tuple for the remaining elements and store it in the last element of the base tuple). To do this generically, it would require either recursion or a reverse loop, creating most nested tuple first.

zett42
  • 25,437
  • 3
  • 35
  • 72
4

This answer was originally posted as my comments in a different answer

You can convert the IP address from a string object to a version object, which "coincidentally" has the same format as an IP address (4 sets of numbers separated by a .)

Get-Content .\abc.txt | Sort-Object { [System.Version]($_).split("-")[1] }
scottwtang
  • 1,740
  • 2
  • 3
  • 13
  • 3
    Yuck. This might be a clever "trick" but misusing a type like this for its sortability should not be a general recommendation for people to actually use. What a shame that this is the highest-voted and accepted answer. Down the road when someone re-asks this question as "Here's how I sort IP address ranges in PowerShell. How do I do it in ?" no one is going to (immediately) understand what `[Version]` is doing in there. Is the lack of code clarity really worth it? I vote no. – Lance U. Matthews May 27 '22 at 19:23
3

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.

Lance U. Matthews
  • 15,725
  • 6
  • 48
  • 68
  • Question out of ignorance, `CompareTo` always has to be static? – Santiago Squarzon May 28 '22 at 16:09
  • 1
    @SantiagoSquarzon The non-`static` `CompareTo` method implements the `IComparable.CompareTo` interface method. The `static` `Compare` method is just a helper method since I compare the `StartAddress` property and then, possibly, the `EndAddress` property of the two `[IPAddressRange]` instances. I could have spun that off into, say, an `[IPAddressComparer]` type that implements the `IComparer` interface, but I thought I'd keep it more self-contained. Is that what you're asking about? – Lance U. Matthews May 28 '22 at 20:30
  • I totally misread `static [Int32] CompareTo` instead of `static [Int32] Compare`, thanks for clarifying! – Santiago Squarzon May 28 '22 at 20:35
2

One simple way would be to split each line on ., take the first part (the first octet of each IP in a range), then cast it as an integer and sort on that.

Get-Content .\MyFile.txt | Sort-Object {$_.Split('.')[0] -as [int]}
TheMadTechnician
  • 34,906
  • 3
  • 42
  • 56
  • Thanks, but that did not completely sort. @scottwtang's comment worked and sorted as expected. – Saeed May 27 '22 at 16:42
  • Strange, it worked for me locally when I tested against your example. – TheMadTechnician May 27 '22 at 16:50
  • @TheMadTechnician I updated my question having your command. – Saeed May 27 '22 at 16:55
  • @SantiagoSquarzon yes, that now works:) – Saeed May 27 '22 at 16:56
  • 1
    @SantiagoSquarzon Yes, that is exactly what was wrong. I didn't copy/paste, and I should have. Updated answer. Not that it matters, he already has a functional answer, but it's better that it works. Thanks for pointing that out! – TheMadTechnician May 27 '22 at 16:59
  • 1
    This works as long as the range start addresses differ in the first octet. My hope was that if a `[ScriptBlock]` returns an array then `Sort-Object` would sort on each successive value; oddly, `... | Sort-Object { [byte[]] ($_ -split '\s*-\s*')[0].Split('.') }` ends up being the same as a lexical sort, though. – Lance U. Matthews May 27 '22 at 20:27
  • quite right! This fails when you have `67.0.42.128 - 67.0.42.255` and `67.0.42.0 - 67.0.42.127`. Good catch. Somebody suggested using `[version]` which seems like a good way to do it now that it's mentioned. – TheMadTechnician May 27 '22 at 21:32
2

scottwang provided a clever way to sort the IPs in a comment, using the Version Class which implements IComparable Interface.

Here is another alternative, clearly less efficient, using a hash table, the IPAddress Class and an array of expressions:

$ips = Get-Content ipfile.txt

$iptable = @{}
foreach($line in $ips) {
    if($ip = $line -replace ' -.+' -as [ipaddress]) {
        $iptable[$line] = $ip.GetAddressBytes()
    }
}

$expressions = foreach($i in 0..3) {
    { $iptable[$_] | Select-Object -Index $i }.GetNewClosure()
}

$ips | Sort-Object $expressions -Descending

Doing the same in a single pipeline is possible with an advanced function or an anonymous function:

Get-Content ipfile.txt | & {
    begin {
        $iptable = @{}
        $expressions = foreach($i in 0..3) {
            { $iptable[$_] | Select-Object -Index $i }.GetNewClosure()
        }
    }
    process {
        if ($ip = $_ -replace ' -.+' -as [ipaddress]) {
            $iptable[$_] = $ip.GetAddressBytes()
        }
    }
    end { $iptable.PSBase.Keys | Sort-Object $expressions -Descending }
}
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    By redefining `$expressions` as `foreach($i in 0..3) { { $_.StartAddressBytes[$i] }.GetNewClosure() }` I was able to rework this into the single pipeline `... | ForEach-Object { if ($ip = $_ -replace ' -.+' -as [ipaddress]) { return [PSCustomObject] @{ Line = $_; StartAddressBytes = $ip.GetAddressBytes() } } } | Sort-Object $expressions -Descending | Select-Object -ExpandProperty 'Line'`. Unfortunately, the line and bytes need to be bundled together for sorting and then split back apart with something like `-ExpandProperty`, so maybe it's a wash. – Lance U. Matthews May 28 '22 at 21:33
  • 1
    Initializing `$iptable` and `$expressions` just as you have and then populating `$iptable` as the lines pass through the pipeline with `... | ForEach-Object { if ($ip = $_ -replace ' -.+' -as [ipaddress]) { $iptable[$_] = $ip.GetAddressBytes(); return $_ } } | Sort-Object $expressions -Descending` seems to be a happy medium, though. – Lance U. Matthews May 28 '22 at 21:34
  • @LanceU.Matthews good call, personally I would use the intrinsic foreach instead of the cmdlet. I'll add the example in a few, thanks for the feedback – Santiago Squarzon May 28 '22 at 21:36
1

My prior answer was quite inefficient hence I decided to provide another alternative using a Class that implements the IComparable Interface and holds an instance of IpAddress:

class IpComparer : IComparable, IEquatable[object] {
    [ipaddress] $IpAddress

    IpComparer([ipaddress] $IpAddress) {
        $this.IpAddress = $IpAddress
    }

    [string] ToString() {
        return $this.IpAddress.ToString()
    }

    [int] GetHashCode() {
        return $this.IpAddress.GetHashCode()
    }

    [bool] Equals([object] $IpAddress) {
        return [IpComparer]::Equals($this, [IpComparer] $IpAddress)
    }

    hidden static [bool] Equals([IpComparer] $LHS, [IpComparer] $RHS) {
        return $LHS.IpAddress.Equals($RHS.IpAddress)
    }

    [int] CompareTo([object] $IpAddress) {
        return [IpComparer]::CompareTo($this, [IpComparer] $IpAddress)
    }

    hidden static [int] CompareTo([IpComparer] $LHS, [IpComparer] $RHS) {
        $x = $LHS.IpAddress.GetAddressBytes()
        $y = $RHS.IpAddress.GetAddressBytes()

        for($i = 0; $i -lt 4; $i++) {
            if($ne = $x[$i].CompareTo($y[$i])) {
                return $ne
            }
        }
        return 0
    }

    hidden static [IpComparer] op_Explicit([string] $IpAddress) {
        return [IpComparer]::new([ipaddress] $IpAddress)
    }
}

Now the instances can be Comparable

[IpComparer] '194.225.0.0' -lt '194.225.15.255'  # => True
[IpComparer] '194.225.15.255' -lt '194.225.0.0'  # => False
[IpComparer] '194.225.0.0' -gt '194.225.15.255'  # => False
[IpComparer] '194.225.15.255' -gt '194.225.0.0'  # => True

Tested for Equality

[IpComparer] '194.225.15.25' -ge '194.225.15.25' # => True
'194.225.15.25' -le [IpComparer] '194.225.15.25' # => True

$hs = [Collections.Generic.HashSet[IpComparer]]::new()
$hs.Add('194.225.0.0') # => True
$hs.Add('194.225.0.0') # => False

([IpComparer[]]('194.225.0.0', '194.225.0.0') | Select-Object -Unique).Count # => 1
([IpComparer] '194.225.15.255').Equals('194.225.15.255') # => True

And, in consequence, Sortable:

Get-Content ipfile.txt | Sort-Object { $_ -replace ' -.+' -as [IpComparer] } -Descending
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
0

Could be something like this.

  Get-Content .\abc.txt |ForEach-Object {($_).split("-")[1]}|Sort-Object
Shabarinath
  • 132
  • 8
  • Thanks, but that's not working as expected. First lines are `100.`, then other lines are `62.` and `78.`. – Saeed May 27 '22 at 15:54
  • @Saeed The IPs are being sorted as a string, so you need to cast them as numbers. The easy way is to add `{[System.Version]$_}` after the `Sort` command in the above answer (i.e. `Sort-Object { [System.Version]$_ }` – scottwtang May 27 '22 at 16:10
  • @scottwtang thanks, but this command removes the first part from `194.225.0.0 - 194.225.15.255`, and then now it has only `194.225.15.255` to sort. – Saeed May 27 '22 at 16:17
  • @Saeed `$ips | Sort-Object { [System.Version]($_).split("-")[1] }` – scottwtang May 27 '22 at 16:32
  • @scottwtang I'm not a well Windows administrator. Could you please guide me more regarding the command? What is `$ips` variable? – Saeed May 27 '22 at 16:34
  • 3
    @Saeed Sorry, that's just my placeholder value that contains the list of IP addresses. The original answer would replace that with `Get-Content .\abc.txt | Sort-Object { [System.Version]($_).split("-")[1] }` – scottwtang May 27 '22 at 16:36
0

I'm just going to use a simple list of ip addresses as an example. The address long integer property of the [ipaddress] type is normally in reverse byte host order (little endian), so has to be changed to left-to-right network byte order (big endian) by HostToNetworkOrder().

$list =
-split '2.3.1.1
1.3.1.1
1.2.1.1'

$list | sort { [ipaddress]::HostToNetworkOrder(([ipaddress]$_).address) }

1.2.1.1
1.3.1.1
2.3.1.1

Note the right side zero padding. '255.255.255.255' wouldn't fit an [int].

[ipaddress]::HostToNetworkOrder(([ipaddress]'1.2.3.4').address) | % tostring x

102030400000000
js2010
  • 23,033
  • 6
  • 64
  • 66