Intuitively, I'd think that if you sort your IP addresses into buckets, one for each subnet, and keep picking 2 from the 2 fullest buckets, that should allow you to pair them all. Could be with something like:
perl -e ' push @{$s{s/\.\d+$//r}}, $_ for @ARGV; @l = values %s; for ($n = @ARGV; $n > 0; $n -= 2) { @l = sort {@$b <=> @$a} @l; printf "ip_map[%s]=%s\n", pop(@{$l[0]}), pop(@{$l[1]}); }' -- "${ips[@]}"
Which on your sample gives:
ip_map[172.211.91.30]=172.211.89.166 ip_map[172.211.90.173]=172.211.91.63 ip_map[172.211.90.61]=172.211.89.233
code for @ARGV loops over the parameters given to the inline script (the -expression) using the default $_ variable as the loop variable. A shorter form of the for (@ARGV) {code}. s/\.\d+$//r (which acts on $_ by default) removes the .<digits> part at the end, but with the r flag, the result is returned instead of being stored back into $_. So that expands to the /24 subnet part of the IP address. - In the first line, we build the
%s associative array (aka hash in perl), where $s{subnet} is a reference to the list of IP addresses in that subnet. @{that} dereferences it so we can push the IP address ($_) onto the list. @l = values %s: gets the values of the hash, that is references to our bucket lists into a @l list. - then, n/2 times, we pluck two IP addresses from the 2 largest subnets, sorting them by size first (when a list is used in scalar context, like in
@$b <=> @$a, it expands to the number of elements in it, so we compare the length of the lists to sort them) and then popping one from the first and second list (pop(@{$l[...]})).
You would evaluate the output in bash (with source <(that-code) or eval "$(that-code)"), but you might as well use perl for the whole thing, shells (especially bash) are not very good at programming.
If I had to use a shell, I would use zsh instead of bash, where something equivalent could look like:
typeset -A s ip_map for ip ($ips) s[${ip%.*}]="x$s[${ip%.*}] $ip" l=( $s ) repeat $#ips/2 { l=( ${(O)l} ) ip_map[${l[1][(w)-1]}]=${l[2][(w)-1]} l[1]=${l[1]#x} l[2]=${l[2]#x} l[1]=${l[1]% *} l[2]=${l[2]% *} }
zsh associative arrays, like bash's can't contain lists, here we're storing the list of IP addresses space separated (so we can use (w)ord-based indexing later on) in the values of the associative array and prefixed with a string of xs (used a bit like 𝍩 𝍪 𝍫 𝍬 𝍸 tally marks), one per IP address so it looks like:
typeset -A s=( [172.211.89]='xx 172.211.89.233 172.211.89.166' [172.211.90]='xx 172.211.90.61 172.211.90.173' [172.211.91]='xx 172.211.91.63 172.211.91.30' )
So the ${(O)l} which we use to Order the list in reverse lexically sorts by number of elements. The popping is done by extracting and removing the last IP addresses in those buckets and remove one x from the start.