pub mod entry;
pub mod merge;
pub mod resolve;
pub mod verify;
use crate::core::model::Row;
use std::collections::HashMap;
use clap::{Parser, Subcommand, ValueHint};
use std::path::PathBuf;
use crate::store::file::DEFAULT_HOSTS_PATH;
#[derive(Parser, Debug)]
#[command(name = "hostab", version, about, long_about = None)]
pub struct Cli {
#[arg(long, env = "HOSTS_FILE", default_value = DEFAULT_HOSTS_PATH, value_hint = ValueHint::FilePath, global = true)]
pub hosts_file: PathBuf,
#[arg(short, long, global = true)]
pub quiet: bool,
#[arg(short = 'o', long, default_value = "table", global = true)]
pub out: String,
#[arg(long, global = true)]
pub no_color: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(subcommand, alias = "e")]
Entry(EntryCommands),
Verify {
#[arg(long)]
strict: bool,
},
Cat,
Resolve {
hosts: Vec<String>,
#[arg(short, long)]
local: bool,
},
Merge {
#[arg(short, long, required = true, value_hint = ValueHint::FilePath)]
src: Vec<String>,
#[arg(short, long, value_hint = ValueHint::FilePath)]
target: Option<PathBuf>,
},
Completion { shell: String },
Version,
}
#[derive(Subcommand, Debug)]
pub enum EntryCommands {
List {
#[arg(long)]
ipv4: bool,
#[arg(long, conflicts_with = "ipv4")]
ipv6: bool,
#[arg(long)]
expand: bool,
#[arg(short = 'f', long = "filter")]
pattern: Option<String>,
#[arg(short = 'i', long)]
ignore_case: bool,
},
Add {
ip: String,
hosts: Vec<String>,
#[arg(long)]
comment: Option<String>,
},
Rm {
hosts: Vec<String>,
#[arg(long)]
ip: Option<String>,
},
Disable {
hosts: Vec<String>,
#[arg(long)]
ip: Option<String>,
},
Enable {
hosts: Vec<String>,
#[arg(long)]
ip: Option<String>,
},
Toggle {
host: String,
#[arg(long)]
ip: Option<String>,
},
Edit {
host: String,
#[arg(long)]
ip: String,
},
}
pub fn compact_rows(rows: &[Row]) -> Vec<Row> {
let mut map: HashMap<String, (Vec<String>, Vec<String>)> = HashMap::new();
let mut order: Vec<String> = Vec::new();
for row in rows {
let key = row.ip.clone();
let (hosts, comments) = map.entry(key.clone()).or_insert_with(|| {
order.push(key.clone());
(Vec::new(), Vec::new())
});
hosts.push(row.host.clone());
if let Some(ref c) = row.comment {
if !c.is_empty() && !comments.contains(c) {
comments.push(c.clone());
}
}
}
order
.into_iter()
.filter_map(|ip| {
map.remove(&ip).map(|(hosts, comments)| Row {
ip,
host: hosts.join(" "),
comment: if comments.is_empty() {
None
} else {
Some(comments.join("; "))
},
})
})
.collect()
}
pub fn print_output(cli: &Cli, rows: &[Row]) {
if cli.quiet {
return;
}
let output_str = match cli.out.as_str() {
"json" => crate::output::json::format_json_rows(rows),
"raw" => crate::output::table::format_raw(rows),
"markdown" => crate::output::table::format_markdown(rows),
_ => crate::output::table::format_table(rows),
};
println!("{}", output_str);
}