Skip to content

Instantly share code, notes, and snippets.

@pmeulen
Last active June 26, 2026 14:10
Show Gist options
  • Select an option

  • Save pmeulen/476208492a9938f408459747247c9902 to your computer and use it in GitHub Desktop.

Select an option

Save pmeulen/476208492a9938f408459747247c9902 to your computer and use it in GitHub Desktop.
Python script to represent a list of CIDR network ranges in the smallest number of CIDR subnets. Subnets can be added and substracted.
#!/usr/bin/env python3
# Copyright 2018, 2026 SURF B.V.
# Requires python 3.10+
import argparse, sys
try:
from netaddr import IPNetwork, IPSet
except ImportError:
print("The netaddr module is required. Install it with e.g. 'pip install netaddr'.")
sys.exit(1)
class IPNetworkArg:
"""Parse an IPv4 or IPv6 network argument from the command line
The IP network can be any network that is supported by netaddr.IPNetwork.
If no netmask is specified, /32 (for IPv4) or /128 (for IPv6) is assumed.
E.g.:
"192.168.0.0/24"
"192.168.0.2" => "192.168.0.2/32"
"2001:db8:abcd::/48"
"2001:db8:abcd:1::10.0.0.1" => "2001:db8:abcd:1::a00:1/128"
The network address can be prefixed with "+" (default) or "-" to specify whether the network
must be added or removed from the range.
Any leading/trailing whitespace and comments (# ...) are ignored.
An empty string is assigned the None network.
Supported """
def __init__(self, network: str):
# Remove comment (# ....) and leading/trailing whitespace
network = network.split('#')[0].strip()
if len(network)==0:
self.__ip_network = None
return
self.__add = True
if network.startswith('-'):
network = network[1:]
self.__add = False # I.e remove
elif network.startswith('+'):
network = network[1:]
try:
self.__ip_network = IPNetwork(network)
except Exception as e:
raise argparse.ArgumentTypeError(f"Invalid IP network specification: \"{network}\". Error: {e}")
@property
def ip_network(self) -> IPNetwork | None:
return self.__ip_network
@property
def add(self) -> bool:
return self.__add
def __str__(self):
if self.__ip_network is None:
return ""
return ("+" if self.add else "-") + self.__ip_network.__str__()
def __bool__(self):
return self.__ip_network is not None
parser = argparse.ArgumentParser(
prefix_chars='/',
fromfile_prefix_chars='@',
description='Represent a list of CIDR network ranges in the smallest number of CIDR subnets. '
'The subnets are added to and subtracted from the final range in the order they are specified on the command line. '
'Example: cidr_merge.py 192.168.0.0/28 -192.168.0.10'
)
parser.add_argument(
dest='cidr_list',
type=IPNetworkArg,
nargs='+',
help='A list of IP Addresses or IP networks in CIDR format (e.g. "192.168.0.0/24"). '
'To subtract a range, prefix it with "-" (e.g. "-192.168.0.3"). '
'To load a list from file use "@filename".'
'Leading/trailing whitespace and comments (# ...) are ignored.',
)
# Show help and exit when no arguments are provided
if len(sys.argv)==1:
parser.print_help()
parser.exit(0)
# Parse arguments
args = parser.parse_args()
# Merge the CIDR ranges
merged_list=IPSet()
for cidr in args.cidr_list:
if not cidr:
continue
if cidr.add:
merged_list.add(cidr.ip_network)
else:
merged_list.remove(cidr.ip_network)
# Print merged list
for cidr in merged_list.iter_cidrs():
print(cidr)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment