ipsubnet 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ipsubnet/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """IP subnetting and addressing calculator."""
2
+
3
+ __version__ = "1.0.1"
ipsubnet/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from ipsubnet.cli import main
4
+
5
+ sys.exit(main())
ipsubnet/cli.py ADDED
@@ -0,0 +1,298 @@
1
+ """Command-line interface for ipsubnet."""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from typing import List, Optional
8
+
9
+ from . import __version__
10
+ from .core import (SubnetError, describe, parse, resolve_new_prefix, subnet_count,
11
+ subnet_rows)
12
+
13
+ DEFAULT_LIMIT = 64
14
+
15
+ EXAMPLES = """\
16
+ examples:
17
+ ipsubnet 192.168.1.10/24
18
+ ipsubnet 192.168.1.10 255.255.255.0
19
+ ipsubnet 10.0.0.0/16 --subnets 8
20
+ ipsubnet 172.16.0.0/22 --hosts 100
21
+ ipsubnet 192.168.0.0/24 --prefix 27
22
+ ipsubnet 2001:db8::1/48 --prefix 52
23
+ ipsubnet 192.168.1.10/24 --json
24
+
25
+ Run without arguments for interactive mode.
26
+ """
27
+
28
+
29
+ class Style:
30
+ """ANSI colours, switched off when output isn't a terminal or NO_COLOR is set."""
31
+
32
+ def __init__(self, enabled: bool):
33
+ self.enabled = enabled
34
+
35
+ def _wrap(self, code: str, text: str) -> str:
36
+ return "\033[{}m{}\033[0m".format(code, text) if self.enabled else text
37
+
38
+ def bold(self, t): return self._wrap("1", t)
39
+ def dim(self, t): return self._wrap("2", t)
40
+ def cyan(self, t): return self._wrap("36", t)
41
+ def green(self, t): return self._wrap("32", t)
42
+ def yellow(self, t): return self._wrap("33", t)
43
+ def red(self, t): return self._wrap("31", t)
44
+ def magenta(self, t): return self._wrap("35", t)
45
+
46
+
47
+ def _enable_windows_ansi() -> bool:
48
+ if os.name != "nt":
49
+ return True
50
+ try:
51
+ import ctypes
52
+ kernel32 = ctypes.windll.kernel32
53
+ handle = kernel32.GetStdHandle(-11)
54
+ mode = ctypes.c_uint32()
55
+ if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
56
+ return False
57
+ return bool(kernel32.SetConsoleMode(handle, mode.value | 0x0004))
58
+ except Exception:
59
+ return False
60
+
61
+
62
+ def make_style(no_color: bool) -> Style:
63
+ if no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
64
+ return Style(False)
65
+ return Style(_enable_windows_ansi())
66
+
67
+
68
+ def fmt_int(n: int) -> str:
69
+ return "{:,}".format(n)
70
+
71
+
72
+ def render_binary(bits: str, prefix: int, s: Style) -> str:
73
+ """Colour network bits and host bits differently."""
74
+ out, seen = [], 0
75
+ for ch in bits:
76
+ if ch == ".":
77
+ out.append(s.dim("."))
78
+ continue
79
+ out.append(s.cyan(ch) if seen < prefix else s.yellow(ch))
80
+ seen += 1
81
+ return "".join(out)
82
+
83
+
84
+ def render_summary(info: dict, s: Style) -> str:
85
+ rows = [("Address", info["address"])]
86
+ rows.append(("Network", s.green(info["cidr"])))
87
+ if info["version"] == 4:
88
+ rows += [
89
+ ("Netmask", "{} (/{})".format(info["netmask"], info["prefix"])),
90
+ ("Wildcard", info["wildcard"]),
91
+ ("Network address", info["network"]),
92
+ ("Broadcast", info["broadcast"]),
93
+ ]
94
+ else:
95
+ rows += [
96
+ ("Expanded", info["expanded"]),
97
+ ("Prefix length", "/{}".format(info["prefix"])),
98
+ ("Network address", info["network"]),
99
+ ]
100
+ rows += [
101
+ ("Usable range", s.green("{} - {}".format(info["first_host"], info["last_host"]))),
102
+ ("Usable hosts", fmt_int(info["usable_hosts"])),
103
+ ("Total addresses", fmt_int(info["total_addresses"])),
104
+ ]
105
+ if info["version"] == 4:
106
+ rows.append(("Class", info["class"]))
107
+ rows.append(("Type", info["scope"]))
108
+
109
+ width = max(len(k) for k, _ in rows)
110
+ lines = [" {} {}".format(s.bold(k.ljust(width)), v) for k, v in rows]
111
+
112
+ if info["version"] == 4:
113
+ b = info["binary"]
114
+ lines += ["", " " + s.bold("Binary") + s.dim(" (network bits | host bits)")]
115
+ for label in ("address", "netmask", "network", "broadcast"):
116
+ lines.append(" {} {}".format(
117
+ label.capitalize().ljust(width),
118
+ render_binary(b[label], info["prefix"], s)))
119
+ return "\n".join(lines)
120
+
121
+
122
+ def render_table(rows: List[dict], s: Style) -> str:
123
+ ipv4 = rows and rows[0]["broadcast"] is not None
124
+ headers = ["#", "Subnet", "First host", "Last host"]
125
+ if ipv4:
126
+ headers.append("Broadcast")
127
+ headers.append("Usable")
128
+
129
+ data = []
130
+ for r in rows:
131
+ line = [str(r["index"]), r["network"], r["first_host"], r["last_host"]]
132
+ if ipv4:
133
+ line.append(r["broadcast"])
134
+ line.append(fmt_int(r["usable_hosts"]))
135
+ data.append(line)
136
+
137
+ widths = [max(len(h), *(len(d[i]) for d in data)) for i, h in enumerate(headers)]
138
+
139
+ def fmt(cells, color=None):
140
+ parts = []
141
+ for i, (c, w) in enumerate(zip(cells, widths)):
142
+ # Right-align numeric columns.
143
+ cell = c.rjust(w) if i in (0, len(cells) - 1) else c.ljust(w)
144
+ parts.append(cell)
145
+ text = " ".join(parts)
146
+ return color(text) if color else text
147
+
148
+ out = [" " + fmt(headers, s.bold), " " + s.dim(" ".join("-" * w for w in widths))]
149
+ out += [" " + fmt(d) for d in data]
150
+ return "\n".join(out)
151
+
152
+
153
+ def run(address: str, mask: Optional[str], new_prefix: Optional[int], subnets: Optional[int],
154
+ hosts: Optional[int], limit: Optional[int], as_json: bool, s: Style) -> None:
155
+ iface, note = parse(address, mask)
156
+ info = describe(iface)
157
+ net = iface.network
158
+
159
+ split_wanted = any(x is not None for x in (new_prefix, subnets, hosts))
160
+ target = resolve_new_prefix(net, new_prefix, subnets, hosts) if split_wanted else None
161
+
162
+ if as_json:
163
+ payload = dict(info)
164
+ if note:
165
+ payload["note"] = note
166
+ if target is not None:
167
+ payload["subnetting"] = {
168
+ "new_prefix": target,
169
+ "subnet_count": subnet_count(net, target),
170
+ "subnets": subnet_rows(net, target, limit),
171
+ "truncated": limit is not None and subnet_count(net, target) > limit,
172
+ }
173
+ print(json.dumps(payload, indent=2))
174
+ return
175
+
176
+ print()
177
+ if note:
178
+ print(" " + s.yellow(note))
179
+ print()
180
+ print(render_summary(info, s))
181
+
182
+ if target is not None:
183
+ count = subnet_count(net, target)
184
+ rows = subnet_rows(net, target, limit)
185
+ per = rows[0]["usable_hosts"]
186
+ print()
187
+ print(" {} {} into /{}: {} subnet{}, {} usable host{} each".format(
188
+ s.bold("Subnets"), info["cidr"], target, fmt_int(count), "" if count == 1 else "s",
189
+ fmt_int(per), "" if per == 1 else "s"))
190
+ print()
191
+ print(render_table(rows, s))
192
+ if count > len(rows):
193
+ print()
194
+ print(" " + s.dim("... showing {} of {} subnets. Use --limit N or --all to see more."
195
+ .format(fmt_int(len(rows)), fmt_int(count))))
196
+ print()
197
+
198
+
199
+ def _ask(prompt: str, s: Style) -> str:
200
+ try:
201
+ return input(s.magenta("? ") + prompt).strip()
202
+ except EOFError:
203
+ raise KeyboardInterrupt from None
204
+
205
+
206
+ def interactive(s: Style, limit: Optional[int]) -> int:
207
+ print()
208
+ print(" " + s.bold("ipsubnet {}".format(__version__)) + " - IP subnetting calculator")
209
+ print(" " + s.dim("Press Enter on an empty line or Ctrl+C to quit."))
210
+ while True:
211
+ print()
212
+ try:
213
+ address = _ask("IP address (e.g. 192.168.1.10 or 192.168.1.10/24): ", s)
214
+ if not address:
215
+ return 0
216
+ mask = None
217
+ if "/" not in address:
218
+ mask = _ask("Subnet mask or CIDR (e.g. 255.255.255.0 or 24, Enter for default): ", s) or None
219
+ split = _ask("Split into subnets? Enter /prefix, N subnets ('8s'), N hosts ('50h'), "
220
+ "or Enter to skip: ", s).lower()
221
+ except KeyboardInterrupt:
222
+ print()
223
+ return 0
224
+
225
+ new_prefix = subnets = hosts = None
226
+ try:
227
+ if split:
228
+ if split.endswith("s") and split[:-1].isdigit():
229
+ subnets = int(split[:-1])
230
+ elif split.endswith("h") and split[:-1].isdigit():
231
+ hosts = int(split[:-1])
232
+ elif split.lstrip("/").isdigit():
233
+ new_prefix = int(split.lstrip("/"))
234
+ else:
235
+ raise SubnetError("didn't understand '{}'".format(split))
236
+ run(address, mask, new_prefix, subnets, hosts, limit, False, s)
237
+ except SubnetError as e:
238
+ print(" " + s.red("error: ") + str(e))
239
+
240
+
241
+ def build_parser() -> argparse.ArgumentParser:
242
+ p = argparse.ArgumentParser(
243
+ prog="ipsubnet",
244
+ description="IP subnetting and addressing calculator for IPv4 and IPv6. "
245
+ "Shows the network address, broadcast, usable host range and "
246
+ "can split a network into subnets.",
247
+ epilog=EXAMPLES,
248
+ formatter_class=argparse.RawDescriptionHelpFormatter,
249
+ )
250
+ p.add_argument("address", nargs="?",
251
+ help="IP address, optionally with CIDR (192.168.1.10/24) or mask (192.168.1.10/255.255.255.0)")
252
+ p.add_argument("mask", nargs="?",
253
+ help="subnet mask or prefix if not included in the address (255.255.255.0 or 24)")
254
+
255
+ g = p.add_argument_group("subnetting (pick one)")
256
+ ex = g.add_mutually_exclusive_group()
257
+ ex.add_argument("-p", "--prefix", type=int, metavar="N", help="split into subnets of prefix /N")
258
+ ex.add_argument("-s", "--subnets", type=int, metavar="N", help="split into at least N equal subnets")
259
+ ex.add_argument("-H", "--hosts", type=int, metavar="N",
260
+ help="split into the smallest subnets that fit N usable hosts each")
261
+
262
+ o = p.add_argument_group("output")
263
+ o.add_argument("-l", "--limit", type=int, default=DEFAULT_LIMIT, metavar="N",
264
+ help="max subnets to list (default {})".format(DEFAULT_LIMIT))
265
+ o.add_argument("-a", "--all", action="store_true", help="list every subnet (can be huge)")
266
+ o.add_argument("-j", "--json", action="store_true", help="print JSON instead of a table")
267
+ o.add_argument("--no-color", action="store_true", help="disable coloured output")
268
+ p.add_argument("-V", "--version", action="version", version="ipsubnet " + __version__)
269
+ return p
270
+
271
+
272
+ def main(argv: Optional[List[str]] = None) -> int:
273
+ args = build_parser().parse_args(argv)
274
+ s = make_style(args.no_color or args.json)
275
+ limit = None if args.all else max(args.limit, 1)
276
+
277
+ if args.address is None:
278
+ if sys.stdin.isatty():
279
+ return interactive(s, limit)
280
+ build_parser().print_help()
281
+ return 2
282
+
283
+ try:
284
+ run(args.address, args.mask, args.prefix, args.subnets, args.hosts, limit, args.json, s)
285
+ except SubnetError as e:
286
+ print("ipsubnet: error: {}".format(e), file=sys.stderr)
287
+ return 1
288
+ except KeyboardInterrupt:
289
+ return 130
290
+ except BrokenPipeError:
291
+ # e.g. `ipsubnet ... --all | head`
292
+ sys.stderr.close()
293
+ return 0
294
+ return 0
295
+
296
+
297
+ if __name__ == "__main__":
298
+ sys.exit(main())
ipsubnet/core.py ADDED
@@ -0,0 +1,244 @@
1
+ """Subnet math: parsing input, describing a network, and splitting it into subnets."""
2
+
3
+ import ipaddress
4
+ import math
5
+ from typing import Iterator, List, Optional, Tuple, Union
6
+
7
+ Interface = Union[ipaddress.IPv4Interface, ipaddress.IPv6Interface]
8
+ Network = Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
9
+
10
+
11
+ class SubnetError(ValueError):
12
+ """Raised for any input the calculator cannot work with."""
13
+
14
+
15
+ def classful_prefix(ip: ipaddress.IPv4Address) -> Optional[int]:
16
+ """Default prefix for the IPv4 address class (A=/8, B=/16, C=/24), or None for D/E."""
17
+ first = int(ip) >> 24
18
+ if first < 128:
19
+ return 8
20
+ if first < 192:
21
+ return 16
22
+ if first < 224:
23
+ return 24
24
+ return None
25
+
26
+
27
+ def _parse_mask(mask: str, version: int) -> int:
28
+ """Turn '24', '/24', '255.255.255.0' or a wildcard like '0.0.0.255' into a prefix length."""
29
+ mask = mask.strip().lstrip("/")
30
+ if not mask:
31
+ raise SubnetError("empty subnet mask")
32
+ if mask.isdigit():
33
+ prefix = int(mask)
34
+ limit = 32 if version == 4 else 128
35
+ if not 0 <= prefix <= limit:
36
+ raise SubnetError("prefix /{} is out of range (0-{})".format(prefix, limit))
37
+ return prefix
38
+ if version == 6:
39
+ raise SubnetError("IPv6 needs a prefix length such as /64, not a dotted mask")
40
+ try:
41
+ # ipaddress accepts both netmasks and wildcard (host) masks here.
42
+ return ipaddress.IPv4Network("0.0.0.0/" + mask).prefixlen
43
+ except ValueError:
44
+ raise SubnetError("'{}' is not a valid subnet mask".format(mask)) from None
45
+
46
+
47
+ def parse(address: str, mask: Optional[str] = None) -> Tuple[Interface, Optional[str]]:
48
+ """Parse user input into an interface (address + prefix).
49
+
50
+ Returns the interface and an optional note to show the user (e.g. when a
51
+ default mask was assumed).
52
+ """
53
+ address = address.strip()
54
+ if "/" in address:
55
+ if mask:
56
+ raise SubnetError("give the mask either as /CIDR or as a separate argument, not both")
57
+ address, mask = address.split("/", 1)
58
+
59
+ try:
60
+ ip = ipaddress.ip_address(address)
61
+ except ValueError:
62
+ raise SubnetError("'{}' is not a valid IPv4 or IPv6 address".format(address)) from None
63
+
64
+ note = None
65
+ if mask is None:
66
+ if ip.version == 4:
67
+ prefix = classful_prefix(ip)
68
+ if prefix is None:
69
+ raise SubnetError(
70
+ "{} is a class D/E address and has no default mask; please give one".format(ip)
71
+ )
72
+ note = "No mask given; assumed the classful default /{}.".format(prefix)
73
+ else:
74
+ prefix = 64
75
+ note = "No prefix given; assumed /64."
76
+ else:
77
+ prefix = _parse_mask(mask, ip.version)
78
+
79
+ return ipaddress.ip_interface("{}/{}".format(ip, prefix)), note
80
+
81
+
82
+ def ip_class(ip: ipaddress.IPv4Address) -> str:
83
+ first = int(ip) >> 24
84
+ if first < 128:
85
+ return "A"
86
+ if first < 192:
87
+ return "B"
88
+ if first < 224:
89
+ return "C"
90
+ if first < 240:
91
+ return "D (multicast)"
92
+ return "E (reserved)"
93
+
94
+
95
+ DOCUMENTATION_NETS = [ipaddress.ip_network(n) for n in (
96
+ "192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24", "2001:db8::/32")]
97
+
98
+
99
+ def ip_scope(ip: Union[ipaddress.IPv4Address, ipaddress.IPv6Address]) -> str:
100
+ if ip.is_loopback:
101
+ return "Loopback"
102
+ if ip.is_link_local:
103
+ return "Link-local"
104
+ if ip.is_multicast:
105
+ return "Multicast"
106
+ if ip.is_unspecified:
107
+ return "Unspecified"
108
+ if any(ip.version == n.version and ip in n for n in DOCUMENTATION_NETS):
109
+ return "Documentation (example range)"
110
+ if ip.version == 4 and ip in ipaddress.IPv4Network("100.64.0.0/10"):
111
+ return "Shared address space (CGNAT)"
112
+ if ip.is_private:
113
+ return "Private"
114
+ if ip.is_reserved:
115
+ return "Reserved"
116
+ if ip.is_global:
117
+ return "Public"
118
+ return "Special-purpose"
119
+
120
+
121
+ def host_range(net: Network) -> Tuple[object, object, int]:
122
+ """First usable, last usable and usable count for a network.
123
+
124
+ IPv4 /31 links use both addresses (RFC 3021); /32 is a single host.
125
+ IPv6 has no broadcast address, so every address is usable.
126
+ """
127
+ total = net.num_addresses
128
+ if net.version == 6 or net.prefixlen >= net.max_prefixlen - 1:
129
+ return net.network_address, net.broadcast_address, total
130
+ return net.network_address + 1, net.broadcast_address - 1, total - 2
131
+
132
+
133
+ def to_binary(ip: ipaddress.IPv4Address) -> str:
134
+ bits = format(int(ip), "032b")
135
+ return ".".join(bits[i:i + 8] for i in range(0, 32, 8))
136
+
137
+
138
+ def describe(iface: Interface) -> dict:
139
+ """All the facts about an address and the network it sits in."""
140
+ net = iface.network
141
+ ip = iface.ip
142
+ first, last, usable = host_range(net)
143
+ info = {
144
+ "version": ip.version,
145
+ "address": str(ip),
146
+ "cidr": "{}/{}".format(net.network_address, net.prefixlen),
147
+ "prefix": net.prefixlen,
148
+ "network": str(net.network_address),
149
+ "first_host": str(first),
150
+ "last_host": str(last),
151
+ "total_addresses": net.num_addresses,
152
+ "usable_hosts": usable,
153
+ "scope": ip_scope(ip),
154
+ }
155
+ if ip.version == 4:
156
+ info.update({
157
+ "netmask": str(net.netmask),
158
+ "wildcard": str(net.hostmask),
159
+ "broadcast": str(net.broadcast_address),
160
+ "class": ip_class(ip),
161
+ "binary": {
162
+ "address": to_binary(ip),
163
+ "netmask": to_binary(net.netmask),
164
+ "network": to_binary(net.network_address),
165
+ "broadcast": to_binary(net.broadcast_address),
166
+ },
167
+ })
168
+ else:
169
+ info.update({
170
+ "netmask": None,
171
+ "wildcard": None,
172
+ "broadcast": None,
173
+ "expanded": ip.exploded,
174
+ })
175
+ return info
176
+
177
+
178
+ def resolve_new_prefix(net: Network, new_prefix: Optional[int] = None,
179
+ subnets: Optional[int] = None, hosts: Optional[int] = None) -> int:
180
+ """Work out the target prefix from exactly one of: new prefix, subnet count, hosts per subnet."""
181
+ given = [x is not None for x in (new_prefix, subnets, hosts)]
182
+ if sum(given) != 1:
183
+ raise SubnetError("choose exactly one of --prefix, --subnets or --hosts")
184
+
185
+ max_len = net.max_prefixlen
186
+ if new_prefix is not None:
187
+ if not net.prefixlen <= new_prefix <= max_len:
188
+ raise SubnetError(
189
+ "new prefix must be between /{} and /{}".format(net.prefixlen, max_len)
190
+ )
191
+ return new_prefix
192
+
193
+ if subnets is not None:
194
+ if subnets < 1:
195
+ raise SubnetError("number of subnets must be at least 1")
196
+ borrowed = math.ceil(math.log2(subnets)) if subnets > 1 else 0
197
+ result = net.prefixlen + borrowed
198
+ if result > max_len:
199
+ raise SubnetError(
200
+ "/{} cannot be split into {} subnets (at most {})".format(
201
+ net.prefixlen, subnets, 2 ** (max_len - net.prefixlen))
202
+ )
203
+ return result
204
+
205
+ if hosts < 1:
206
+ raise SubnetError("hosts per subnet must be at least 1")
207
+ # IPv4 subnets reserve network + broadcast (textbook rule; a single host gets /32).
208
+ if net.version == 4 and hosts > 1:
209
+ needed = hosts + 2
210
+ else:
211
+ needed = hosts
212
+ host_bits = math.ceil(math.log2(needed)) if needed > 1 else 0
213
+ result = max_len - host_bits
214
+ if result < net.prefixlen:
215
+ raise SubnetError(
216
+ "/{} is too small for {} hosts per subnet (it has {} usable)".format(
217
+ net.prefixlen, hosts, host_range(net)[2])
218
+ )
219
+ return result
220
+
221
+
222
+ def split(net: Network, new_prefix: int) -> Iterator[Network]:
223
+ return net.subnets(new_prefix=new_prefix)
224
+
225
+
226
+ def subnet_count(net: Network, new_prefix: int) -> int:
227
+ return 2 ** (new_prefix - net.prefixlen)
228
+
229
+
230
+ def subnet_rows(net: Network, new_prefix: int, limit: Optional[int]) -> List[dict]:
231
+ rows = []
232
+ for i, sub in enumerate(split(net, new_prefix), 1):
233
+ if limit is not None and i > limit:
234
+ break
235
+ first, last, usable = host_range(sub)
236
+ rows.append({
237
+ "index": i,
238
+ "network": str(sub),
239
+ "first_host": str(first),
240
+ "last_host": str(last),
241
+ "broadcast": str(sub.broadcast_address) if sub.version == 4 else None,
242
+ "usable_hosts": usable,
243
+ })
244
+ return rows
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: ipsubnet
3
+ Version: 1.0.1
4
+ Summary: IP subnetting and addressing calculator for the command line (IPv4 and IPv6)
5
+ Author: Shazan Hussain Zaidi, Harshit Agrawal, Prabhat Bhati
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ShazanZaidii/ipsubnet
8
+ Keywords: subnet,cidr,ip,network,calculator,cli
9
+ Classifier: Environment :: Console
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: System :: Networking
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # ipsubnet
20
+
21
+ An IP subnetting and addressing calculator for the command line. Give it an IP address with a subnet mask or CIDR prefix. It prints the network address, broadcast address, usable host range and host counts, and it can split the network into a list of subnets. It works with IPv4 and IPv6 on Windows, macOS and Linux.
22
+
23
+ ## Install
24
+
25
+ **Windows** (PowerShell):
26
+
27
+ ```powershell
28
+ irm https://raw.githubusercontent.com/ShazanZaidii/ipsubnet/main/scripts/install.ps1 | iex
29
+ ```
30
+
31
+ **macOS / Linux**:
32
+
33
+ ```sh
34
+ curl -fsSL https://raw.githubusercontent.com/ShazanZaidii/ipsubnet/main/scripts/install.sh | sh
35
+ ```
36
+
37
+ **With Python** (any OS, Python 3.8+):
38
+
39
+ ```sh
40
+ pip install ipsubnet
41
+ ```
42
+
43
+ or, to keep it isolated from other packages, `pipx install ipsubnet`.
44
+
45
+ You can also download a ready-to-run binary from the [Releases page](https://github.com/ShazanZaidii/ipsubnet/releases/latest):
46
+
47
+ | OS | File |
48
+ |---|---|
49
+ | Windows | `ipsubnet-windows-x64.exe` |
50
+ | macOS (Apple Silicon) | `ipsubnet-macos-arm64` |
51
+ | macOS (Intel) | `ipsubnet-macos-x64` |
52
+ | Linux (x64) | `ipsubnet-linux-x64` |
53
+ | Linux (ARM64) | `ipsubnet-linux-arm64` |
54
+
55
+ On macOS and Linux, run `chmod +x` on the file first. On macOS, if you downloaded it with a browser, also run `xattr -d com.apple.quarantine <file>`.
56
+
57
+ ## Usage
58
+
59
+ ```
60
+ ipsubnet ADDRESS[/PREFIX] [MASK] [--prefix N | --subnets N | --hosts N] [options]
61
+ ```
62
+
63
+ | Input | Meaning |
64
+ |---|---|
65
+ | `192.168.1.10/24` | Address with a CIDR prefix |
66
+ | `192.168.1.10 255.255.255.0` | Address and a dotted subnet mask |
67
+ | `192.168.1.10 24` | Address and a prefix length |
68
+ | `192.168.1.10` | Uses the classful default mask (A=/8, B=/16, C=/24) |
69
+ | `2001:db8::1/48` | IPv6 |
70
+
71
+ | Option | Description |
72
+ |---|---|
73
+ | `-p, --prefix N` | Split into subnets of size /N |
74
+ | `-s, --subnets N` | Split into at least N equal subnets |
75
+ | `-H, --hosts N` | Split into the smallest subnets that fit N usable hosts each |
76
+ | `-l, --limit N` | Show at most N subnets (default 64) |
77
+ | `-a, --all` | Show every subnet |
78
+ | `-j, --json` | Output JSON |
79
+ | `--no-color` | Turn off colours |
80
+
81
+ If you run `ipsubnet` with no arguments, it starts in interactive mode and asks for each value. This is also what happens when you double-click the Windows `.exe`.
82
+
83
+ ### Example
84
+
85
+ ```
86
+ $ ipsubnet 192.168.1.10/24 --hosts 50
87
+
88
+ Address 192.168.1.10
89
+ Network 192.168.1.0/24
90
+ Netmask 255.255.255.0 (/24)
91
+ Wildcard 0.0.0.255
92
+ Network address 192.168.1.0
93
+ Broadcast 192.168.1.255
94
+ Usable range 192.168.1.1 - 192.168.1.254
95
+ Usable hosts 254
96
+ Total addresses 256
97
+ Class C
98
+ Type Private
99
+
100
+ Binary (network bits | host bits)
101
+ Address 11000000.10101000.00000001.00001010
102
+ Netmask 11111111.11111111.11111111.00000000
103
+ Network 11000000.10101000.00000001.00000000
104
+ Broadcast 11000000.10101000.00000001.11111111
105
+
106
+ Subnets 192.168.1.0/24 into /26: 4 subnets, 62 usable hosts each
107
+
108
+ # Subnet First host Last host Broadcast Usable
109
+ - ---------------- ------------- ------------- ------------- ------
110
+ 1 192.168.1.0/26 192.168.1.1 192.168.1.62 192.168.1.63 62
111
+ 2 192.168.1.64/26 192.168.1.65 192.168.1.126 192.168.1.127 62
112
+ 3 192.168.1.128/26 192.168.1.129 192.168.1.190 192.168.1.191 62
113
+ 4 192.168.1.192/26 192.168.1.193 192.168.1.254 192.168.1.255 62
114
+ ```
115
+
116
+ ## How it works
117
+
118
+ - **Network address**: the IP AND the subnet mask (all host bits set to 0).
119
+ - **Broadcast address**: the network address with all host bits set to 1.
120
+ - **Usable hosts**: 2^(32 − prefix) − 2 for IPv4, because the network and broadcast addresses can't be assigned to hosts. A /31 has 2 usable addresses (point-to-point links, RFC 3021) and a /32 has 1.
121
+ - **Subnetting**: borrowing *b* host bits creates 2^b subnets of prefix + *b*.
122
+ - `--subnets N` borrows ⌈log₂ N⌉ bits.
123
+ - `--hosts N` keeps ⌈log₂(N + 2)⌉ host bits.
124
+ - **IPv6** has no broadcast address, so every address in the prefix is counted as usable.
125
+
126
+ ## Development
127
+
128
+ ```sh
129
+ pip install -e .
130
+ python -m unittest discover -s tests -v
131
+ ```
132
+
133
+ Pushing a tag like `v1.0.1` makes GitHub Actions run the tests on all three operating systems, build the binaries and publish a release.
134
+
135
+ ## Authors
136
+
137
+ Group 4: Shazan Hussain Zaidi, Harshit Agrawal, Prabhat Bhati
@@ -0,0 +1,10 @@
1
+ ipsubnet/__init__.py,sha256=CPSYTvFM_bjsaEJ5Vc3gqyKJC4rAMwOA2WdxODyxl_s,70
2
+ ipsubnet/__main__.py,sha256=VDDt6PjW8FfNVs0yz8nIRMQ5bhcldgv9menjS59yQsA,60
3
+ ipsubnet/cli.py,sha256=p5Ehgn8jQfdpt5kuJgBxE4JDnSF9SGZuIjQfJMNyeSk,10775
4
+ ipsubnet/core.py,sha256=xElqJyUEVyuAa3BlONamKqhtXFIV_AvNIy2YyDwCnfg,8409
5
+ ipsubnet-1.0.1.dist-info/licenses/LICENSE,sha256=nwyiEOEuxSwucb81U5Pf-swWbfXtX6AWrGp9WlIKebY,1109
6
+ ipsubnet-1.0.1.dist-info/METADATA,sha256=61l1JrbBQKwAs9wVjFHetgV1r3hybPlZ18-pn9GUuRo,5003
7
+ ipsubnet-1.0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ ipsubnet-1.0.1.dist-info/entry_points.txt,sha256=L9vLkbQeV78mwA1uSunCoPJ7rgtCVJ5xIBAGbbOLzAQ,47
9
+ ipsubnet-1.0.1.dist-info/top_level.txt,sha256=4sv85yzG4QCydDa5rhkKnHcPNVngGRBhkxlfJyDpsu8,9
10
+ ipsubnet-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ipsubnet = ipsubnet.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shazan Hussain Zaidi, Harshit Agrawal, Prabhat Bhati
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ ipsubnet