Skip to content

Commit 2485b86

Browse files
committed
feat: Enable read from stdin
1 parent 7bbb0d1 commit 2485b86

File tree

1 file changed

+160
-38
lines changed

1 file changed

+160
-38
lines changed

src/main.rs

Lines changed: 160 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// src/main.rs
22
use clap::Parser;
33
use futures::stream::{self, StreamExt};
4-
use serde::Serialize;
5-
use std::time::Duration;
4+
use serde::{Serialize, Deserialize};
5+
use std::path::PathBuf;
6+
use std::fs;
7+
use std::io::{self, BufRead};
8+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
69
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
710
use trust_dns_resolver::TokioAsyncResolver;
811

@@ -13,20 +16,32 @@ use trust_dns_resolver::TokioAsyncResolver;
1316
version
1417
)]
1518
struct Cli {
16-
/// Domain names to check
17-
#[arg(required = true)]
19+
/// Domain names to check (optional if reading from stdin)
20+
#[arg(required = false)]
1821
domains: Vec<String>,
1922

2023
/// Maximum number of concurrent checks
2124
#[arg(short, long, default_value = "10")]
2225
concurrent: usize,
2326

24-
/// Output as JSON
27+
/// Output as JSON to stdout
2528
#[arg(short, long)]
2629
json: bool,
30+
31+
/// Save output to JSON file
32+
#[arg(long)]
33+
output_file: Option<PathBuf>,
34+
35+
/// Include timestamp in output
36+
#[arg(short, long)]
37+
timestamp: bool,
38+
39+
/// Strip whitespace and empty lines from input
40+
#[arg(short, long)]
41+
clean: bool,
2742
}
2843

29-
#[derive(Debug, Serialize)]
44+
#[derive(Debug, Serialize, Deserialize)]
3045
struct DomainStatus {
3146
domain: String,
3247
registered: bool,
@@ -37,6 +52,22 @@ struct DomainStatus {
3752
error: Option<String>,
3853
}
3954

55+
#[derive(Debug, Serialize, Deserialize)]
56+
struct CheckResult {
57+
timestamp: Option<u64>,
58+
check_count: usize,
59+
domains: Vec<DomainStatus>,
60+
summary: ResultSummary,
61+
}
62+
63+
#[derive(Debug, Serialize, Deserialize)]
64+
struct ResultSummary {
65+
total_checked: usize,
66+
registered: usize,
67+
unregistered: usize,
68+
errors: usize,
69+
}
70+
4071
struct DomainChecker {
4172
resolver: TokioAsyncResolver,
4273
}
@@ -47,7 +78,10 @@ impl DomainChecker {
4778
opts.timeout = Duration::from_secs(2);
4879
opts.attempts = 2;
4980

50-
let resolver = TokioAsyncResolver::tokio(ResolverConfig::cloudflare(), opts);
81+
let resolver = TokioAsyncResolver::tokio(
82+
ResolverConfig::cloudflare(),
83+
opts,
84+
);
5185

5286
Self { resolver }
5387
}
@@ -68,7 +102,10 @@ impl DomainChecker {
68102
Ok(ns_records) => {
69103
status.has_dns = true;
70104
status.registered = true;
71-
status.nameservers = ns_records.iter().map(|record| record.to_string()).collect();
105+
status.nameservers = ns_records
106+
.iter()
107+
.map(|record| record.to_string())
108+
.collect();
72109
}
73110
Err(e) => match e.kind() {
74111
trust_dns_resolver::error::ResolveErrorKind::NoRecordsFound { .. } => {}
@@ -85,7 +122,10 @@ impl DomainChecker {
85122
Ok(ips) => {
86123
status.has_ip = true;
87124
status.registered = true;
88-
status.ip_addresses = ips.iter().map(|ip| ip.to_string()).collect();
125+
status.ip_addresses = ips
126+
.iter()
127+
.map(|ip| ip.to_string())
128+
.collect();
89129
}
90130
Err(e) => {
91131
if !status.registered {
@@ -102,11 +142,7 @@ impl DomainChecker {
102142
status
103143
}
104144

105-
async fn check_domains(
106-
&self,
107-
domains: Vec<String>,
108-
concurrent_limit: usize,
109-
) -> Vec<DomainStatus> {
145+
async fn check_domains(&self, domains: Vec<String>, concurrent_limit: usize) -> Vec<DomainStatus> {
110146
stream::iter(domains)
111147
.map(|domain| self.check_domain(domain))
112148
.buffer_unordered(concurrent_limit)
@@ -115,39 +151,125 @@ impl DomainChecker {
115151
}
116152
}
117153

154+
fn create_check_result(domains: Vec<DomainStatus>, include_timestamp: bool) -> CheckResult {
155+
let total_checked = domains.len();
156+
let registered = domains.iter().filter(|d| d.registered).count();
157+
let unregistered = domains.iter().filter(|d| !d.registered).count();
158+
let errors = domains.iter().filter(|d| d.error.is_some()).count();
159+
160+
CheckResult {
161+
timestamp: if include_timestamp {
162+
Some(SystemTime::now()
163+
.duration_since(UNIX_EPOCH)
164+
.unwrap()
165+
.as_secs())
166+
} else {
167+
None
168+
},
169+
check_count: total_checked,
170+
domains,
171+
summary: ResultSummary {
172+
total_checked,
173+
registered,
174+
unregistered,
175+
errors,
176+
},
177+
}
178+
}
179+
180+
fn print_text_output(result: &CheckResult) {
181+
if let Some(timestamp) = result.timestamp {
182+
println!("\nTimestamp: {}", timestamp);
183+
}
184+
println!("\nSummary:");
185+
println!(" Total Checked: {}", result.summary.total_checked);
186+
println!(" Registered: {}", result.summary.registered);
187+
println!(" Unregistered: {}", result.summary.unregistered);
188+
println!(" Errors: {}", result.summary.errors);
189+
190+
println!("\nDetailed Results:");
191+
for status in &result.domains {
192+
println!("\nDomain: {}", status.domain);
193+
println!("Registered: {}", status.registered);
194+
195+
if !status.nameservers.is_empty() {
196+
println!("Nameservers:");
197+
for ns in &status.nameservers {
198+
println!(" - {}", ns);
199+
}
200+
}
201+
202+
if !status.ip_addresses.is_empty() {
203+
println!("IP Addresses:");
204+
for ip in &status.ip_addresses {
205+
println!(" - {}", ip);
206+
}
207+
}
208+
209+
if let Some(error) = &status.error {
210+
println!("Error: {}", error);
211+
}
212+
}
213+
}
214+
215+
fn read_domains_from_stdin(clean: bool) -> io::Result<Vec<String>> {
216+
let stdin = io::stdin();
217+
let mut domains = Vec::new();
218+
219+
for line in stdin.lock().lines() {
220+
let line = line?;
221+
if clean {
222+
let trimmed = line.trim();
223+
if !trimmed.is_empty() {
224+
domains.push(trimmed.to_string());
225+
}
226+
} else {
227+
domains.push(line);
228+
}
229+
}
230+
231+
Ok(domains)
232+
}
233+
118234
#[tokio::main]
119235
async fn main() -> Result<(), Box<dyn std::error::Error>> {
120236
let cli = Cli::parse();
121237
let checker = DomainChecker::new().await;
122238

123-
let results = checker.check_domains(cli.domains, cli.concurrent).await;
124-
125-
if cli.json {
126-
println!("{}", serde_json::to_string_pretty(&results)?);
239+
// Get domains from either command line args or stdin
240+
let domains = if cli.domains.is_empty() {
241+
// No domains provided as arguments, try reading from stdin
242+
read_domains_from_stdin(cli.clean)?
127243
} else {
128-
for status in results {
129-
println!("\nDomain: {}", status.domain);
130-
println!("Registered: {}", status.registered);
131-
132-
if !status.nameservers.is_empty() {
133-
println!("Nameservers:");
134-
for ns in status.nameservers {
135-
println!(" - {}", ns);
136-
}
137-
}
244+
cli.domains
245+
};
138246

139-
if !status.ip_addresses.is_empty() {
140-
println!("IP Addresses:");
141-
for ip in status.ip_addresses {
142-
println!(" - {}", ip);
143-
}
144-
}
247+
// Verify we have domains to check
248+
if domains.is_empty() {
249+
eprintln!("Error: No domains provided. Either specify domains as arguments or pipe them through stdin.");
250+
std::process::exit(1);
251+
}
145252

146-
if let Some(error) = status.error {
147-
println!("Error: {}", error);
148-
}
253+
let results = checker
254+
.check_domains(domains, cli.concurrent)
255+
.await;
256+
257+
let check_result = create_check_result(results, cli.timestamp);
258+
259+
// Handle output based on flags
260+
if cli.json || cli.output_file.is_some() {
261+
let json = serde_json::to_string_pretty(&check_result)?;
262+
263+
if cli.json {
264+
println!("{}", json);
265+
}
266+
267+
if let Some(path) = cli.output_file {
268+
fs::write(path, json)?;
149269
}
270+
} else {
271+
print_text_output(&check_result);
150272
}
151273

152274
Ok(())
153-
}
275+
}

0 commit comments

Comments
 (0)