Skip to content

Commit 99cb9fd

Browse files
authored
Improvements (#5)
* Fix IPv6 :: expansion and add tests * Add CHANGELOG * Linked CHANGELOG in README * Fix IPv6 parsing / broadcast and expand tests * Fix IPv6 host range and add regression tests * Fix IPv4 /0 handling and tighten IPv4 parsing * Add IPv6 enhancements, netmask CIDR, JSON CLI and tests * Add JSON metadata, stdin/compact CLI modes, and docs/tests
1 parent 4854121 commit 99cb9fd

8 files changed

Lines changed: 743 additions & 113 deletions

File tree

CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Changelog
2+
3+
## 0.1.5
4+
- Fix IPv6 `::` expansion to produce correct 8-segment addresses and add regression tests.
5+
- Fix IPv6 broadcast calculation for non-byte-aligned CIDR and add regression test.
6+
- Fix IPv6 host range to be computed from the network address and add regression test.
7+
- Fix IPv4 /0 netmask and host range calculation and add regression test.
8+
- Reject IPv4 segments that contain trailing junk (e.g., `/24`).
9+
- Add IPv6 compressed output formatting and IPv4-mapped IPv6 parsing with stricter validation.
10+
- Expand IPv6 scope detection and /127 host range behavior.
11+
- Support IPv4 netmask notation in CIDR parsing.
12+
- Add `--json` and `--ip` CLI options.
13+
- Add tests for compressed IPv6 forms, non-byte-aligned IPv6 ranges, IPv4 /31, and netmask parsing.
14+
- Add JSON schema/version fields and stdin/compact/no-color CLI modes.
15+
- Add tests and docs for IPv4-mapped IPv6 and stdin/JSON output.
16+
- Add changelog and bump displayed version to 0.1.5.
17+
18+
## 0.1.4
19+
- Development (#4).
20+
- Correct version metadata.
21+
22+
## 0.1.3
23+
- Development (#3).
24+
- Add GitHub Actions workflow for build and test on release.
25+
- Enable manual triggering of build and test workflow.
26+
- Remove FreeBSD from build matrix and update dependency installation steps.
27+
- Update C++ standard from 26 to 23 in CMakeLists.txt.
28+
- Include `<limits>` header for IPv4 network calculation.
29+
- Disable fail-fast in build matrix for improved workflow stability.
30+
31+
## 0.1.2
32+
- Add IPv4 network and broadcast calculation methods (#2).
33+
34+
## 0.1.1
35+
- Add IPv6 support (#1).
36+
- Fix a README mistake.
37+
38+
## 0.1.0
39+
- Add `cmake` directory.
40+
- Initial commit.

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ IP Analyzer is a command-line tool that provides detailed information about IP a
1010
- Calculate usable IP range and number of hosts
1111
- Determine if the IP address is private
1212
- Present results in a colorful, easy-to-read format
13+
- Support IPv4 netmask notation (e.g., `/255.255.255.0`)
14+
- Offer JSON output and non-interactive `--ip` and `--stdin` modes
15+
- Provide `--compact` and `--no-color` output modes
1316

1417
## Prerequisites
1518

@@ -57,6 +60,20 @@ Enter an IP address with CIDR notation: 192.168.178.0/24
5760

5861
When prompted, enter an IP address with or without CIDR notation. The tool will automatically detect whether it's an IPv4 or IPv6 address.
5962

63+
You can also run non-interactively:
64+
65+
```bash
66+
./build/ip-analyzer --ip 192.168.1.1/24 --json
67+
```
68+
69+
The JSON output includes a schema identifier (`ip-analyzer/1`) and the app version.
70+
71+
You can process multiple inputs from stdin:
72+
73+
```bash
74+
printf "192.168.1.1/24\n2001:db8::1/64\n" | ./build/ip-analyzer --stdin --compact
75+
```
76+
6077
## Examples
6178

6279
### IPv4 Example
@@ -71,14 +88,18 @@ The IP Analyzer supports various IPv6 address formats, including:
7188

7289
- Full notation: `2001:0db8:85a3:0000:0000:8a2e:0370:7334`
7390
- Compressed notation: `2001:db8:85a3::8a2e:370:7334`
74-
- IPv4-mapped IPv6 addresses: `::ffff:192.0.2.128`
91+
- IPv4-mapped IPv6 addresses: `::ffff:192.0.2.128` (outputs as `::ffff:192.0.2.128`)
7592

7693
The program will display detailed information about the IP address.
7794

7895
## License
7996

8097
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
8198

99+
## Changelog
100+
101+
Release notes are tracked in [CHANGELOG.md](CHANGELOG.md).
102+
82103
## Pull Requests
83104

84105
If you find a bug or want to contribute to the project, feel free to submit a pull request.

src/cli_app.cc

Lines changed: 202 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@
88

99
namespace ip_analyzer {
1010

11-
void PrintCopperBar()
11+
void PrintCopperBar(bool color_enabled)
1212
{
13+
if (!color_enabled)
14+
{
15+
fmt::print("{}\n", std::string(kWidth, '='));
16+
return;
17+
}
18+
1319
const auto copper_gradient = [](int i)
1420
{
1521
constexpr int kMaxColor = 255;
@@ -26,30 +32,66 @@ void PrintCopperBar()
2632
fmt::print("\n");
2733
}
2834

29-
void PrintHeader(const std::string &text)
35+
void PrintHeader(const std::string &text, bool color_enabled)
3036
{
31-
PrintCopperBar();
32-
fmt::print(OutputColors::kHeader, "{:^{}}\n", text, kWidth);
33-
PrintCopperBar();
37+
PrintCopperBar(color_enabled);
38+
if (color_enabled)
39+
{
40+
fmt::print(OutputColors::kHeader, "{:^{}}\n", text, kWidth);
41+
}
42+
else
43+
{
44+
fmt::print("{:^{}}\n", text, kWidth);
45+
}
46+
PrintCopperBar(color_enabled);
3447
}
3548

36-
void PrintRow(const std::string &label, const std::string &value, const std::string &binary)
49+
void PrintRow(const std::string &label, const std::string &value, const std::string &binary,
50+
bool color_enabled)
3751
{
38-
fmt::print(OutputColors::kLabel, "{:<20}", label);
52+
if (color_enabled)
53+
{
54+
fmt::print(OutputColors::kLabel, "{:<20}", label);
55+
}
56+
else
57+
{
58+
fmt::print("{:<20}", label);
59+
}
3960
if (binary.empty())
4061
{
41-
fmt::print(OutputColors::kValue, "{}\n", value);
62+
if (color_enabled)
63+
{
64+
fmt::print(OutputColors::kValue, "{}\n", value);
65+
}
66+
else
67+
{
68+
fmt::print("{}\n", value);
69+
}
4270
}
4371
else
4472
{
45-
fmt::print(OutputColors::kValue, "{:<20}", value);
46-
fmt::print(OutputColors::kBinary, "{}\n", binary);
73+
if (color_enabled)
74+
{
75+
fmt::print(OutputColors::kValue, "{:<20}", value);
76+
fmt::print(OutputColors::kBinary, "{}\n", binary);
77+
}
78+
else
79+
{
80+
fmt::print("{:<20}{}\n", value, binary);
81+
}
4782
}
4883
}
4984

5085
void IPAnalyzerApp::PrintPrompt() const
5186
{
52-
fmt::print(OutputColors::kPrompt, "Enter IP address with CIDR (e.g., 192.168.0.1/24): ");
87+
if (color_enabled_)
88+
{
89+
fmt::print(OutputColors::kPrompt, "Enter IP address with CIDR (e.g., 192.168.0.1/24): ");
90+
}
91+
else
92+
{
93+
fmt::print("Enter IP address with CIDR (e.g., 192.168.0.1/24): ");
94+
}
5395
}
5496

5597
void IPAnalyzerApp::PrintVersion() const
@@ -65,14 +107,23 @@ void IPAnalyzerApp::PrintHelp() const
65107
fmt::print("Options:\n");
66108
fmt::print(" -h, --help Show this help message and exit\n");
67109
fmt::print(" -v, --version Show version information and exit\n\n");
110+
fmt::print(" --json Output results as JSON\n");
111+
fmt::print(" --compact Use compact, non-decorated output\n");
112+
fmt::print(" --no-color Disable colored output\n");
113+
fmt::print(" --stdin Read IP/CIDR values from stdin\n");
114+
fmt::print(" --ip <value> Provide the IP/CIDR without prompting\n\n");
68115
fmt::print("Examples:\n");
69116
fmt::print(" {} 192.168.1.1/24\n", kAppName);
70117
fmt::print(" {} 2001:db8::1/64\n", kAppName);
118+
fmt::print(" {} 2001:db8::/64\n", kAppName);
71119
}
72120

73121
void IPAnalyzerApp::PrintResults(const IPAnalyzer &analyzer) const
74122
{
75-
PrintHeader("IP Analysis Results");
123+
if (!compact_)
124+
{
125+
PrintHeader("IP Analysis Results", color_enabled_);
126+
}
76127

77128
const auto ip = analyzer.get_ip();
78129
const auto [first, last] = analyzer.get_host_range();
@@ -95,20 +146,147 @@ void IPAnalyzerApp::PrintResults(const IPAnalyzer &analyzer) const
95146
rows.emplace_back("Broadcast Address", analyzer.get_broadcast()->to_string(), "");
96147
}
97148

98-
for (const auto &[label, value, binary] : rows)
149+
if (compact_)
150+
{
151+
for (const auto &[label, value, binary] : rows)
152+
{
153+
fmt::print("{}: {}\n", label, value);
154+
}
155+
}
156+
else
157+
{
158+
for (const auto &[label, value, binary] : rows)
159+
{
160+
PrintRow(label, value, binary, color_enabled_);
161+
}
162+
}
163+
164+
if (!compact_)
165+
{
166+
PrintCopperBar(color_enabled_);
167+
}
168+
}
169+
170+
void IPAnalyzerApp::PrintJsonResults(const IPAnalyzer &analyzer) const
171+
{
172+
PrintJsonObject(analyzer, "", false);
173+
}
174+
175+
void IPAnalyzerApp::PrintJsonObject(const IPAnalyzer &analyzer, const std::string &indent,
176+
bool trailing_comma) const
177+
{
178+
const auto ip = analyzer.get_ip();
179+
const auto [first, last] = analyzer.get_host_range();
180+
181+
fmt::print("{}{{\n", indent);
182+
fmt::print("{} \"schema\": \"ip-analyzer/1\",\n", indent);
183+
fmt::print("{} \"version\": \"{}\",\n", indent, kVersion);
184+
fmt::print("{} \"ip\": \"{}\",\n", indent, ip->to_string());
185+
fmt::print("{} \"network\": \"{}\",\n", indent, analyzer.get_network()->to_string());
186+
fmt::print("{} \"netmask\": \"{}\",\n", indent, analyzer.get_netmask()->to_string());
187+
fmt::print("{} \"cidr\": {},\n", indent, analyzer.get_cidr());
188+
fmt::print("{} \"range_first\": \"{}\",\n", indent, first->to_string());
189+
fmt::print("{} \"range_last\": \"{}\",\n", indent, last->to_string());
190+
fmt::print("{} \"num_hosts\": {},\n", indent, analyzer.get_num_hosts());
191+
fmt::print("{} \"private\": {},\n", indent, analyzer.is_private() ? "true" : "false");
192+
193+
if (ip->is_ipv6())
194+
{
195+
fmt::print("{} \"scope\": \"{}\"\n", indent, GetIPv6Scope(ip));
196+
}
197+
else
198+
{
199+
fmt::print("{} \"broadcast\": \"{}\"\n", indent, analyzer.get_broadcast()->to_string());
200+
}
201+
202+
fmt::print("{}}}{}\n", indent, trailing_comma ? "," : "");
203+
}
204+
205+
int IPAnalyzerApp::RunFromStdin()
206+
{
207+
std::vector<std::string> inputs;
208+
std::string line;
209+
while (std::getline(std::cin, line))
210+
{
211+
if (!line.empty())
212+
{
213+
inputs.push_back(line);
214+
}
215+
}
216+
217+
if (inputs.empty())
99218
{
100-
PrintRow(label, value, binary);
219+
return 1;
101220
}
102221

103-
PrintCopperBar();
222+
if (output_json_)
223+
{
224+
fmt::print("[\n");
225+
}
226+
227+
for (size_t index = 0; index < inputs.size(); ++index)
228+
{
229+
try
230+
{
231+
IPAnalyzer analyzer(inputs[index]);
232+
if (output_json_)
233+
{
234+
PrintJsonObject(analyzer, " ", index + 1 < inputs.size());
235+
}
236+
else
237+
{
238+
if (index > 0)
239+
{
240+
fmt::print("\n");
241+
}
242+
PrintResults(analyzer);
243+
}
244+
}
245+
catch (const std::exception &e)
246+
{
247+
if (output_json_)
248+
{
249+
fmt::print("]\n");
250+
}
251+
PrintError(e.what());
252+
return 1;
253+
}
254+
}
255+
256+
if (output_json_)
257+
{
258+
fmt::print("]\n");
259+
}
260+
261+
return 0;
104262
}
105263

106264
std::string IPAnalyzerApp::GetIPv6Scope(const std::shared_ptr<IPAddress> &ip) const
107265
{
108266
auto ipv6 = std::dynamic_pointer_cast<IPv6Address>(ip);
109267
auto bytes = ipv6->to_bytes();
268+
bool is_unspecified = true;
269+
for (auto byte : bytes)
270+
{
271+
if (byte != 0)
272+
{
273+
is_unspecified = false;
274+
break;
275+
}
276+
}
277+
if (is_unspecified)
278+
return "Unspecified";
279+
if (bytes[0] == 0 && bytes[1] == 0 && bytes[2] == 0 && bytes[3] == 0 &&
280+
bytes[4] == 0 && bytes[5] == 0 && bytes[6] == 0 && bytes[7] == 0 &&
281+
bytes[8] == 0 && bytes[9] == 0 && bytes[10] == 0 && bytes[11] == 0 &&
282+
bytes[12] == 0 && bytes[13] == 0 && bytes[14] == 0 && bytes[15] == 1)
283+
return "Loopback";
284+
if (bytes[0] == 0x20 && bytes[1] == 0x01 && bytes[2] == 0x0d && bytes[3] == 0xb8)
285+
return "Documentation";
110286
if (bytes[0] == 0xfe && (bytes[1] & 0xc0) == 0x80)
111287
return "Link-Local";
288+
if (bytes[0] == 0xfe && (bytes[1] & 0xc0) == 0xc0)
289+
return "Site-Local";
112290
if (bytes[0] == 0xfd || bytes[0] == 0xfc)
113291
return "Unique Local";
114292
if (bytes[0] == 0xff)
@@ -118,7 +296,14 @@ std::string IPAnalyzerApp::GetIPv6Scope(const std::shared_ptr<IPAddress> &ip) co
118296

119297
void IPAnalyzerApp::PrintError(const std::string &message) const
120298
{
121-
fmt::print(OutputColors::kError, "Error: {}\n", message);
299+
if (color_enabled_)
300+
{
301+
fmt::print(OutputColors::kError, "Error: {}\n", message);
302+
}
303+
else
304+
{
305+
fmt::print("Error: {}\n", message);
306+
}
122307
}
123308

124-
}
309+
}

0 commit comments

Comments
 (0)