use anyhow::Result;
use bssh::cli::{
Cli, Commands, PdshCli, has_pdsh_compat_flag, is_pdsh_compat_mode, remove_pdsh_compat_flag,
};
use bssh::hostlist;
use clap::Parser;
use glob::Pattern;
mod app;
use app::{
cache::handle_cache_stats, dispatcher::dispatch_command, initialization::initialize_app,
query::handle_query, utils::show_usage,
};
#[tokio::main]
async fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let pdsh_mode = is_pdsh_compat_mode() || has_pdsh_compat_flag(&args);
if pdsh_mode {
return run_pdsh_mode(&args).await;
}
run_bssh_mode(&args).await
}
async fn run_pdsh_mode(args: &[String]) -> Result<()> {
let filtered_args = if has_pdsh_compat_flag(args) {
remove_pdsh_compat_flag(args)
} else {
args.to_vec()
};
let pdsh_cli = PdshCli::parse_from(filtered_args.iter());
if pdsh_cli.is_query_mode() {
return handle_pdsh_query_mode(&pdsh_cli).await;
}
let mut cli = pdsh_cli.to_bssh_cli();
if cli.hosts.is_none() {
eprintln!("Error: No hosts specified. Use -w to specify target hosts.");
eprintln!("Usage: pdsh -w hosts command");
std::process::exit(1);
}
if cli.command_args.is_empty() {
eprintln!("Error: No command specified.");
eprintln!("Usage: pdsh -w hosts command");
std::process::exit(1);
}
let ctx = initialize_app(&mut cli, args).await?;
dispatch_command(&cli, &ctx).await
}
async fn handle_pdsh_query_mode(pdsh_cli: &PdshCli) -> Result<()> {
if let Some(ref hosts_str) = pdsh_cli.hosts {
let hosts: Vec<String> = hostlist::expand_host_specs(hosts_str)
.map_err(|e| anyhow::anyhow!("Failed to expand host expression: {e}"))?;
let (expanded_exclusions, glob_exclusions): (Vec<String>, Vec<Pattern>) = if let Some(
ref exclude_str,
) =
pdsh_cli.exclude
{
let mut expanded = Vec::new();
let mut globs = Vec::new();
for pattern in exclude_str.split(',').map(|s| s.trim()) {
const MAX_PATTERN_LENGTH: usize = 256;
if pattern.len() > MAX_PATTERN_LENGTH {
anyhow::bail!(
"Exclusion pattern too long (max {MAX_PATTERN_LENGTH} characters)"
);
}
if pattern.is_empty() {
continue;
}
if hostlist::is_hostlist_expression(pattern) {
let expanded_hosts = hostlist::expand_host_specs(pattern)
.map_err(|e| anyhow::anyhow!("Failed to expand exclusion pattern: {e}"))?;
expanded.extend(expanded_hosts);
} else {
let wildcard_count = pattern.chars().filter(|c| *c == '*' || *c == '?').count();
const MAX_WILDCARDS: usize = 10;
if wildcard_count > MAX_WILDCARDS {
anyhow::bail!(
"Exclusion pattern contains too many wildcards (max {MAX_WILDCARDS})"
);
}
match Pattern::new(pattern) {
Ok(p) => globs.push(p),
Err(_) => {
anyhow::bail!("Invalid exclusion pattern: {pattern}");
}
}
}
}
(expanded, globs)
} else {
(Vec::new(), Vec::new())
};
let exclusion_set: std::collections::HashSet<&str> =
expanded_exclusions.iter().map(|s| s.as_str()).collect();
for host in &hosts {
let is_excluded_by_hostlist = exclusion_set.contains(host.as_str());
let is_excluded_by_glob = glob_exclusions.iter().any(|pattern| {
let pattern_str = pattern.as_str();
if !pattern_str.contains('*')
&& !pattern_str.contains('?')
&& !pattern_str.contains('[')
{
host == pattern_str || host.contains(pattern_str)
} else {
pattern.matches(host)
}
});
if !is_excluded_by_hostlist && !is_excluded_by_glob {
println!("{host}");
}
}
} else {
eprintln!("Error: No hosts specified for query mode.");
eprintln!("Usage: pdsh -w hosts -q");
std::process::exit(1);
}
Ok(())
}
async fn run_bssh_mode(args: &[String]) -> Result<()> {
if args.len() == 1 {
show_usage();
std::process::exit(0);
}
let mut cli = Cli::parse();
if let Some(ref query) = cli.query {
handle_query(query);
return Ok(());
}
if matches!(cli.command, Some(Commands::List))
|| (cli.is_multi_server_mode() && cli.destination.as_deref() == Some("list"))
{
let config = bssh::config::Config::load_with_priority(&cli.config).await?;
bssh::commands::list::list_clusters(&config);
return Ok(());
}
if let Some(Commands::CacheStats {
detailed,
clear,
maintain,
}) = &cli.command
{
handle_cache_stats(*detailed, *clear, *maintain).await;
return Ok(());
}
let ctx = initialize_app(&mut cli, args).await?;
dispatch_command(&cli, &ctx).await
}