6

I have a bunch of IP-addresses stored in an array, e.g.:

my @ip = qw(10.11.1.1 10.100.1.1 ...);

How can I sort the addresses in the ascending order? I've tried a simple sort but it failed, of course.

planetp
  • 14,248
  • 20
  • 86
  • 160

8 Answers8

12

IPv4 addresses are just 32-bit numbers.

use Socket qw( inet_aton );
my @sorted =
    map substr($_, 4),
       sort
          map inet_aton($_) . $_,
             @ips;

or

my @sorted =
    map substr($_, 4),
       sort
          map pack('C4a*', split(/\./), $_),
             @ips;

The first one also accepts domain names.

ikegami
  • 367,544
  • 15
  • 269
  • 518
  • Ironically, I didn't use the IP addresses in their numerical form, but as packed numbers. To get an IPv4 address as a number, use `unpack 'N'` on the result of `inet_aton($_)` or `pack('C4', split(/\./, $_))`. – ikegami Aug 02 '11 at 20:59
  • @ysth, Thanks. I figure if it's worth doing with ST, it's worth doing with GRT (although I can never remember what it's called). – ikegami Aug 03 '11 at 18:38
  • Nice solution, but it took me a few moments to figure out how it actually works. I added an answer here that contains my notes, hope you don't mind. – jippie Jan 27 '17 at 18:05
8

I'm not fond of any solution that assumes more that it needs. I've been burned on this sort of thing by compact notation before, and I imagine this problem gets tougher when IPv6 becomes more common. I'd just let Net::IP figure it out:

use 5.010;
use Net::IP;

my @ip = qw(
    192.168.1.10
    172.16.5.5
    256.0.0.1
    192.168.1/24
    127.1
    127.0.1
    fd00:6587:52d7:f8f7:5a55:caff:fef5:af31
    fd24:cd9b:f001:884c:5a55:caff:fef5:af31 
    );

my @sorted = 
    map  { $_->[0] }
    sort { $a->[1] <=> $b->[1] }
    map  { [ $_, eval { Net::IP->new( $_ )->intip } ] }
    @ip;

say join "\n", @sorted;

This handles the compact and range notations just fine and the eval catches the bad IP addresses. I don't have to treat IPv4 and IPv6 separately:

256.0.0.1
127.0.1
127.1
172.16.5.5
192.168.1/24
192.168.1.10
fd00:6587:52d7:f8f7:5a55:caff:fef5:af31
fd24:cd9b:f001:884c:5a55:caff:fef5:af31
brian d foy
  • 129,424
  • 31
  • 207
  • 592
  • 2
    the eval may catch exceptions from bad IP addresses but then leaves you comparing undef, giving warnings – ysth Aug 03 '11 at 05:23
  • Except this doesn't correctly sort mixed collections of IPv4 and IPv6 addresses. (For example, `192.168.1.10 ::ffff:192.168.1.11 192.168.1.12` aren't returned in that order.) It also accepts a lot more than IP addresses (though that may not be an issue), but not domain names. – ikegami Dec 20 '17 at 22:02
2

use Sort::Key::IPv4

salva
  • 9,943
  • 4
  • 29
  • 57
1

I was looking @ikegami's answer which turned out to work perfectly, but I had no clue why. So I took couple moments to figure out the mechanics behind it and I want to share my notes for future reference for the lesser Perl experts ...

In this example I chose two very specific IP addresses because when encoded as ASCII they'll look like ABCD and EFGH, as seen by the output of the print Dumper() line.

The trick is to prefix every IP-address string with 4 bytes containing its binary representation. Then the entries are sorted and finally the prefix is removed again, leaving a list of sorted IP-addresses.

The inner workings are described in the comments, best to read them in the numbered order.

#!/usr/bin/perl

use warnings;
use strict;
use Data::Dumper;

my @ips = qw( 69.70.71.72 65.66.67.68 );

print Dumper( map( pack( 'C4a*' , split( /\./ ) , $_ ) , @ips ) );

foreach my $ip (
  map(                            # 5. For each IP address that was enriched with 32 bits representation ....
    substr( $_ , 4) ,             # 6. Snip off the first four bytes as this is just a binary representation of the string, used for sorting.
    sort(                         # 4. Sort will essentially work on the first 4 octets as these will represent the entire remainder of the string.
      map(                        # 1. For each IP address in @ARGV ...
        pack( 'C4a*' ,            # 3. Change ASCII encoded octets from the IP address into a 32 bit 'string' (that can be sorted) and append it with the original IP address string for later use.
          split( /\./ ), $_ ) ,   # 2. Split the IP address in separate octets
        @ips                      # 0. Unsorted list of IP addresses.
      )    
    )
  )
) {
    print "$ip\n";
}

The output will look as follows:

$VAR1 = 'EFGH69.70.71.72';
$VAR2 = 'ABCD64.65.66.67';
64.65.66.67
69.70.71.72

Where the first two lines are from print Dumper() which shows the IP- addresses are prefixed with a 32-bit representation of the numeric IP-addresses.

jippie
  • 937
  • 5
  • 15
  • 33
1
sort -n -t . -k 1,1 -k 2,2 -k 3,3 -k 4,4 filename  

or | to sort -n -t . -k 1,1 -k 2,2 -k 3,3 -k 4,4

and you can reverse it... -r :-)

Warcupine
  • 4,460
  • 3
  • 15
  • 24
unix guy
  • 11
  • 1
1

Just IPv4 addresses?

my @ip = map $_->[0],
    sort { $a->[1] cmp $b->[1] }
    map [ $_, join '', map chr, split /\./, $_ ],
    qw( 10.1.2.3 172.20.1.2 192.168.1.2 );
brian d foy
  • 129,424
  • 31
  • 207
  • 592
ysth
  • 96,171
  • 6
  • 121
  • 214
0

This should give you a good start:

#!/usr/bin/perl

use strict;
use warnings;

sub Compare {
    # TODO: Error checking
    my @a  = split /\./, $a;
    my @b = split /\./, $b;
    # TODO: This might be cleaner as a loop
    return $a[0] <=> $b[0] ||
           $a[1] <=> $b[1] ||
           $a[2] <=> $b[2] ||
           $a[3] <=> $b[3];
}

my @ip = qw( 172.20.1.2 10.10.2.3 10.1.2.3 192.168.1.2 );

@ip = sort Compare @ip;

print join("\n", @ip), "\n";
Keith Thompson
  • 254,901
  • 44
  • 429
  • 631
0

There's a module designed to sort software version numbers. Maybe that will do what you want?

AmbroseChapel
  • 11,957
  • 7
  • 46
  • 68