Skip to content
shellhacks.dev
Text Processing

How to Sort IP Addresses and Version Numbers Correctly

Sort IP addresses and version numbers correctly with sort -V, sort -t. -k keys, and a zero-padding fallback, and see why plain sort gets them wrong.

6 min read
Summarize in AI
Illustration for "How to Sort IP Addresses and Version Numbers Correctly": a terminal session, text before and after a transform, file permission bits.

The default sort looks at characters, one after another, so numbers inside text are compared as text. That puts 10.0.0.10 before 10.0.0.9 and version 1.10 before 1.9, which surprises people reading a report. Two options fix it. The output below is from GNU coreutils 9.4; the general rules for sorting columns are in sorting by column.

See the problem

Shell
printf '%s\n' 10.0.0.9 10.0.0.10 10.0.0.2 9.1.1.1 | sort
Text
10.0.0.10
10.0.0.2
10.0.0.9
9.1.1.1

Each line is compared character by character: 1 before 2 before 9, so 10.0.0.10 comes before 10.0.0.2, and 9.1.1.1 goes last because 9 is greater than 1 as a character. The sorted result is correct as text and wrong for readers.

Version sort

Shell
printf '%s\n' 10.0.0.9 10.0.0.10 10.0.0.2 9.1.1.1 | sort -V
printf '%s\n' v1.10.0 v1.9.2 v1.9.10 v1.2.0 | sort -V
Text
9.1.1.1
10.0.0.2
10.0.0.9
10.0.0.10
v1.2.0
v1.9.2
v1.9.10
v1.10.0

-V compares runs of digits as numbers and the rest as text, which suits both IPv4 addresses and software versions. It is a GNU extension, also present in recent BSD sort and in macOS from version 12 or so; check with sort --version. Pre-release suffixes such as 1.0-rc1 sort after 1.0 with -V, which is not what semantic versioning says, so handle those separately when they matter.

Key-based sort for IPv4

Shell
sort -t. -k1,1n -k2,2n -k3,3n -k4,4n ips.txt
sort -t. -k1,1n -k2,2n -k3,3n -k4,4n -u ips.txt

-t. sets the field separator to a dot, and each -kN,Nn sorts one field numerically. The explicit end position, 1,1, matters: -k1n would run the key to the end of the line. This works on any POSIX sort. For lines with extra text, such as 10.0.0.9 web1, split on dots only if the address is at the start, or extract the address first, as in extracting IPs from text.

Pad and sort when -V is missing

Shell
awk -F. '{printf "%03d.%03d.%03d.%03d %s\n", $1, $2, $3, $4, $0}' ips.txt | sort | cut -d' ' -f2-

Padding each field to three digits turns numeric order into text order, so a plain sort works, and the original line is carried along and restored with cut. This is the decorate, sort and undecorate pattern, and it works with any tool that has plain sort. For version strings of unknown shape it is harder, since the number of parts varies.

Top values and duplicates

Shell
grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' access.log | sort -V | uniq -c | sort -rn | head

The first sort puts equal addresses next to each other for uniq -c, and the second ranks by count, as covered in top values with sort and uniq. Use sort -V in the first step only if you also want the list in address order for ties.

Why numeric and version order differ from text order

A comparison by characters treats the digit one as smaller than the digit two, and so places "10" before "2". Numeric sort parses a run of digits as a number first, and version sort goes further, alternating between text and numeric parts across the whole string, so it can order "1.9.10" after "1.9.2" and "file9" before "file10". Each needs the right tool for the data. A plain number column wants sort -n. A mixture of digits and text, like versions, host names with numbers and build identifiers, wants -V. Fixed-width fields can use a text sort when they are zero-padded. Choosing the method by the shape of the data avoids surprises, and printing a short sample before and after is the best check.

Locale affects the result too. In locales other than C, sort uses collation rules that can ignore punctuation or case when comparing, so two machines with different LANG values sort the same file differently. For data that must be ordered the same everywhere, set LC_ALL=C on the command, which compares bytes. That also makes sort faster on large files.

Pre-release and special version strings

Real version strings are more complex than three numbers. Pre-releases like 1.0.0-rc1 must come before 1.0.0 under semantic versioning, and sort -V places the plain version before the suffixed one in some releases, because it treats the suffix as extra text that makes the string longer and therefore later. Debian-style versions with epochs and tildes follow their own rules, and dpkg --compare-versions implements them exactly. For semantic versions, tools such as semver or a short script in Python using packaging.version give the correct ordering. Use sort -V for quick scans of simple dotted numbers, and a version-aware library whenever the order drives a decision such as which release to install.

Sorting IPv6 and CIDR notation

IPv6 addresses need parsing before comparison, because the same address has several textual forms: leading zeros can be dropped, a run of zero groups can be replaced by a double colon, and hexadecimal digits may be upper or lower case. Convert to a canonical form first. Python's ipaddress module does it in a few lines, including for networks with prefix lengths: python3 -c 'import sys, ipaddress; print(*sorted(map(ipaddress.ip_interface, sys.stdin.read().split())), sep="\n")'. For IPv4 CIDR blocks such as 10.0.0.0/24, sorting with dots as separators plus the prefix as a fifth key works with sort -t. -k1,1n -k2,2n -k3,3n -k4,4n when the last dotted field is separated from the slash by a second pass. The tool sort -V handles the slash form acceptably for most inventories, with the caveat that overlapping networks are not merged or detected.

Stability, uniqueness and large inputs

When two lines compare equal on the chosen keys, their order is not defined unless you ask for stable behaviour with -s. That matters when sorting by one column and wanting the existing order within ties. The -u flag removes lines that compare equal on the key, not only identical lines, so sort -u -k1,1 keeps the first line for each distinct key. Large files are handled by external merge sort, using temporary files in $TMPDIR, which should have enough space, and can be pointed elsewhere with -T. Setting --parallel and -S 1G raises the speed on big inputs. Check the free space before sorting a file of several gigabytes on a small root disk.

Putting it into a script

Name the order in a comment and test the command on a short sample that includes the awkward cases: single-digit and multi-digit parts, equal prefixes, and a pre-release. Keep the sort as late as possible in the pipeline, after filtering, so it works on less data, and send the result to a file with a clear name. If other programs read it, state the ordering in the documentation of the file format, since they may rely on it.

Where this does not apply

IPv6 addresses have compressed forms and hexadecimal groups, so neither -V nor numeric keys give address order; use a tool that parses them, such as Python's ipaddress module with sorted(map(ipaddress.ip_address, lines)). Locale settings change how text is ordered, so set LC_ALL=C in scripts where order must be byte-wise. For CIDR blocks, sort by address and then prefix length. The options are defined in the GNU sort manual and the POSIX sort specification.

Taggedsort
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.