Home > Net >  Powershell sort IP Addresses in txt file
Powershell sort IP Addresses in txt file

Time:05-28

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

CodePudding user response:

A simple solution using RegEx: 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 {
    # Split the current line on anything that is not a digit.
    # Parse the first four numbers as int's.
    [int[]] $octets = ($_ -split '\D', 5)[ 0..3 ]
    
    # Create and output a tuple that consists of the numbers
    [Tuple]::Create( $octets[0], $octets[1], $octets[2], $octets[3] )
}
  • The -split operator creates an array by splitting the input string by the given regular expression pattern. In this case the pattern \D splits on any character that is not a number. The number 5 after the pattern defines the maximum number of output array elements. It is not 4, because the last part is the remainder, containing the substring after the first 4 numbers.
  • By simply assigning the string array created by -split to a variable with the [int[]] type constraint, 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.

CodePudding user response:

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] }

CodePudding user response:

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.

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

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)
    }

In either case, 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.

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 -le $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]::Compare($this.StartAddress, $other.StartAddress)
        if ($result -eq 0)
        {
            $result = [IPAddressRange]::Compare($this.EndAddress, $other.EndAddress)
        }

        return $result
    }

    hidden static [Int32] Compare([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.

CodePudding user response:

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]}

CodePudding user response:

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

CodePudding user response:

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 : System.IComparable {
    [ipaddress] $IPAddress

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

    [int] CompareTo ([object] $Ip) {
        return $this.CompareTo([IpComparer] $Ip)
    }
    [int] CompareTo ([IpComparer] $Ip) {
        $lhs = $this.IPAddress.GetAddressBytes()
        $rhs = $Ip.IPAddress.GetAddressBytes()

        for($i = 0; $i -lt 4; $i  ) {
            if($lhs[$i] -eq $rhs[$i]) {
                continue
            }
            if($lhs[$i] -gt $rhs[$i]) {
                return 1
            }
            return -1
        }
        return 0
    }
}

Now the instances can be easily 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

And, in consequence, sortable:

Get-Content ipfile.txt | Sort-Object { $_ -replace ' -. ' -as [IpComparer] } -Descending

CodePudding user response:

Could be something like this.

  Get-Content .\abc.txt |ForEach-Object {($_).split("-")[1]}|Sort-Object
  • Related