netdig 0.0.5

Utilities for analyzing and aggregating CIDR blocks
Documentation
use anyhow::Result;
#[cfg(feature = "tracing")]
use clap::ArgAction;
use clap::Parser;
#[cfg(feature = "json")]
use netdig::Json;
use netdig::{IpSet, Tabular};
use std::io;
use strum::EnumString;
#[cfg(feature = "tracing")]
use tracing::{debug, trace};

#[derive(Debug, Parser)]
struct Cli {
    #[cfg(feature = "tracing")]
    #[clap(short, long, action = ArgAction::Count, global = true)]
    verbose: u8,

    /// Format the output for display.
    #[clap(short, long, default_value = "text", global = true)]
    format: Format,

    /// Aggregate subnets together into the minimum number of blocks that
    /// include all subnets. Prints the aggregated list and doesn't run WHOIS
    /// queries.
    #[clap(short, long)]
    aggregate: bool,

    /// Truncate any addresses to the cananonical address for the subnet. Prints
    /// the truncated list and doesn't run WHOIS queries.
    #[clap(short, long)]
    truncate: bool,

    /// Skip running the WHOIS query, and just summarize/validate the subnets.
    #[clap(long)]
    no_whois: bool,

    ips: Vec<String>,
}

#[derive(Debug, Clone, Copy, EnumString)]
#[strum(serialize_all = "snake_case")]
enum Format {
    Text,
    #[cfg(feature = "json")]
    Json,
}

#[allow(clippy::too_many_lines)]
fn main() -> Result<()> {
    let cli = Cli::parse();

    #[cfg(feature = "tracing")]
    {
        jacklog::from_level!(cli.verbose + 2)?;
        debug!(?cli);
    }

    // If the ips argument was empty, then we need to read the IPs from STDIN.
    let mut ips = IpSet::default();
    if cli.ips.is_empty() {
        let lines = io::stdin().lines();
        for line in lines {
            let line = line?;
            let line = line.trim();
            #[cfg(feature = "tracing")]
            trace!(?line);

            if line.is_empty() {
                continue;
            }

            ips.insert(line.to_string())?;
        }
    } else {
        for ip in cli.ips {
            ips.insert(ip)?;
        }
    };

    let outputs = ips.check_ips(!cli.no_whois)?;

    // If the user just wants the aggregated subnets, display those.
    if cli.aggregate {
        for out in outputs {
            let Some(agg) = out.contained_by else {
                continue;
            };

            println!("{agg}");
        }

        return Ok(());
    }

    // If the user just wants the truncated, canonical form of the subnet, print
    // those and return.
    if cli.truncate {
        for out in outputs {
            println!("{}", out.canonical);
        }

        return Ok(());
    }

    match cli.format {
        #[cfg(feature = "json")]
        Format::Json => {
            println!("{}", outputs.to_json()?);
        },

        Format::Text => {
            for line in outputs.table(atty::is(atty::Stream::Stdout)).lines() {
                println!("{}", line.trim());
            }
        },
    }

    Ok(())
}