Skip to content

mfinelli/ipsort

Repository files navigation

ipsort

CI

ipsort sorts IP addresses and CIDR blocks by their actual numeric value rather than lexicographically. It handles plain lists, YAML and JSON list items, inline config values, and other mixed content, preserving surrounding decoration while reordering only the addresses.

Features

  • Sorts IPv4 and IPv6 addresses numerically
  • Preserves decoration: YAML list markers, JSON brackets, inline keys, punctuation
  • Block separator model: non-IP lines (comments, blank lines) divide input into independently sorted groups
  • --inline mode for sorting IPs spread across multiple lines within a single logical unit
  • --aggregate to merge adjacent CIDRs into their minimal supernet representation
  • --unique deduplication by normalized CIDR
  • --normalize for canonical network string output
  • --ips-only and --ips-only-with-structure for extracting bare addresses
  • Composable in shell pipelines

Installation

From a release

Download the binary for your platform from the releases page, make it executable, and place it somewhere on your PATH:

chmod +x ipsort
mv ipsort ~/.local/bin/

Shell completions and the manpage are included in the release archive.

From source

Requires Rust and scdoc (for the manpage).

git clone https://github.com/mfinelli/ipsort.git
cd ipsort
make

The compiled binary, shell completions, and manpage are written to target/release/. To build and install to your system:

sudo make install    # installs to /usr/local by default
sudo make uninstall  # removes installed files

Usage

ipsort [OPTIONS] [ADDRESS...]

Read from stdin when no addresses are given. Use - to read explicitly from stdin.

Options

Flag Short Description
--reverse -r Reverse the sort order
--ipv6-first Sort IPv6 before IPv4 in mixed input
--unique -u Deduplicate by normalized CIDR, keeping first occurrence
--aggregate -a Merge adjacent CIDRs into their minimal supernet representation
--check -c Exit 0 if input is already sorted/aggregated/unique, 1 otherwise
--inline -i Sort all IPs globally across the entire input
--normalize -n Emit canonical network strings (clears host bits, adds /32//128)
--ips-only Strip decoration, emit one bare IP per line, discard non-IP lines
--ips-only-with-structure Strip decoration, emit one bare IP per line, preserve non-IP lines

--ips-only and --ips-only-with-structure are mutually exclusive.

Exit codes

Code Meaning
0 Success
1 --check ran successfully; input is not in the expected state
2 Operational error (no input, no IPs found, conflicting flag usage)

Examples

Sort a list from stdin:

$ printf '192.168.1.0/24\n10.0.0.0/8\n172.16.0.0/12\n' | ipsort
10.0.0.0/8
172.16.0.0/12
192.168.1.0/24

Sort positional arguments:

$ ipsort 192.168.1.0/24 10.0.0.0/8 172.16.0.0/12
10.0.0.0/8
172.16.0.0/12
192.168.1.0/24

Sort a YAML list, preserving list decoration:

$ cat <<EOF | ipsort
- 192.168.1.0/24
- 10.0.0.0/8
- 172.16.0.0/12
EOF
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.1.0/24

Sort with block separators preserved:

$ cat <<EOF | ipsort
# internal ranges
192.168.1.0/24
10.0.0.0/8

# dmz
172.16.2.0/24
172.16.1.0/24
EOF
# internal ranges
10.0.0.0/8
192.168.1.0/24

# dmz
172.16.1.0/24
172.16.2.0/24

Sort IPs from a JSON field using jq:

$ jq '.networks[]' config.json | ipsort

Read from a file using shell redirection:

$ ipsort < addresses.txt

Sort a file in place using sponge from moreutils:

$ ipsort < addresses.txt | sponge addresses.txt

Without sponge, redirecting to the same file you are reading from will truncate it before ipsort reads it. sponge buffers the output and writes it only after the input is fully read.

Sort an entire YAML file, leaving all structure intact:

$ cat firewall.yml
# firewall rules
allowed_sources:
  - 192.168.1.0/24
  - 10.0.0.0/8
  - 172.16.5.0/24

denied_sources:
  - 10.99.0.0/16
  - 192.168.99.0/24

$ ipsort < firewall.yml
# firewall rules
allowed_sources:
  - 10.0.0.0/8
  - 172.16.5.0/24
  - 192.168.1.0/24

denied_sources:
  - 10.99.0.0/16
  - 192.168.99.0/24

Comments, blank lines, and YAML keys are all preserved exactly. Only the IP addresses are reordered, independently within each block separated by blank lines.

Sort IPs in a Kubernetes ingress annotation with --inline:

$ ipsort --inline < ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-service
  annotations:
    nginx.ingress.kubernetes.io/whitelist-source-range: >-
      10.0.0.0/8,
      10.99.0.0/16,
      172.16.5.0/24,
      192.168.1.0/24
spec:
  rules:
  - host: my-service.example.com

The entire YAML structure is preserved. --inline treats all IPs across the file as one pool, which is what you want when a list spans multiple lines within a single annotation value.

Sort a multi-line YAML value with --inline:

$ cat <<EOF | ipsort --inline
allowed_ips: 192.168.1.0/24 10.0.0.0/8
  172.16.2.0/24 172.16.1.0/24
EOF
allowed_ips: 10.0.0.0/8 172.16.1.0/24
  172.16.2.0/24 192.168.1.0/24

Deduplicate a list:

$ printf '10.0.0.0/8\n192.168.0.0/16\n10.0.0.0/8\n' | ipsort --unique
10.0.0.0/8
192.168.0.0/16

Aggregate subnets into their minimal supernet:

$ cat <<EOF | ipsort --aggregate
- 10.0.0.0/25
- 10.0.0.128/25
- 192.168.0.0/24
EOF
- 10.0.0.0/24
- 192.168.0.0/24

Normalize to canonical form:

$ printf '10.0.0.5/24\n192.168.1.1\n' | ipsort --normalize
10.0.0.0/24
192.168.1.1/32

Extract bare IPs, discarding all decoration and structure:

$ cat <<EOF | ipsort --ips-only
# group one
- 192.168.1.0/24
- 10.0.0.0/8

# group two
- 172.16.0.0/12
EOF
10.0.0.0/8
172.16.0.0/12
192.168.1.0/24

Check whether a file is already sorted (useful in CI):

$ ipsort --check < addresses.txt && echo "sorted" || echo "not sorted"

With other flags, checks whether the input satisfies those conditions:

$ ipsort --check --unique < addresses.txt  # exits 1 if duplicates exist
$ ipsort --check --aggregate < addresses.txt  # exits 1 if CIDRs can be merged

Sort in reverse:

$ ipsort --reverse 10.0.0.0/8 172.16.0.0/12 192.168.1.0/24
192.168.1.0/24
172.16.0.0/12
10.0.0.0/8

Input format

ipsort tokenizes each line by splitting on any character that cannot appear in a valid CIDR address, so it handles all common delimiter styles without configuration:

Input style Example
Space-separated 10.0.0.0/8 192.168.0.0/16
Comma-separated 10.0.0.0/8,192.168.0.0/16
Quoted JSON-style "10.0.0.0/8", "192.168.0.0/16"
YAML list items - 10.0.0.0/8
Inline config values network: 10.0.0.0/8

Bare IP addresses (without a prefix length) are accepted and treated as /32 or /128 for sorting. CIDRs with host bits set (e.g. 10.0.0.5/24) are accepted with a warning to stderr (host bits are cleared for sorting), and the original string is preserved in output unless --normalize is set.

Contributing

Contributions are welcome. The codebase is structured as a library (src/lib.rs) with a thin CLI binary (src/main.rs). The library modules are:

  • parse: token-level CIDR parsing
  • classify: line spanification and classification
  • sort: sort comparator and options
  • blocks: block-level and inline sorting, deduplication
  • output: output rendering

For a full account of the design decisions, requirements, and tradeoffs behind the implementation, see DESIGN.md.

Run the test suite with:

cargo test

License

Licensed under the GNU GPL version 3.0 or later.

ipsort: versitile ip address sorting tool
Copyright 2026 Mario Finelli

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

About

versitile ip address sorting tool

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages