use clap::Parser;
use std::path::Path;
#[derive(Parser, Debug, Clone)]
#[command(
name = "pdsh",
version,
about = "Parallel distributed shell (bssh compatibility mode)",
long_about = "bssh running in pdsh compatibility mode.\n\n\
This allows bssh to accept pdsh-style command line arguments.\n\
All pdsh options are mapped to their bssh equivalents.",
after_help = "EXAMPLES:\n \
pdsh -w host1,host2 \"uptime\" # Execute on hosts\n \
pdsh -w host[1-3] -f 10 \"df -h\" # Fanout of 10\n \
pdsh -w nodes -x badnode \"cmd\" # Exclude host\n \
pdsh -w hosts -N \"hostname\" # No hostname prefix\n \
pdsh -w hosts -q # Query mode (show hosts)\n \
pdsh -w hosts -l admin \"cmd\" # Specify user\n\n\
Note: This is bssh running in pdsh compatibility mode.\n\
For full bssh features, run 'bssh --help'."
)]
pub struct PdshCli {
#[arg(short = 'w', help = "Target hosts (comma-separated or range notation)")]
pub hosts: Option<String>,
#[arg(short = 'x', help = "Exclude hosts from target list")]
pub exclude: Option<String>,
#[arg(
short = 'f',
default_value = "32",
help = "Fanout (parallel connections)"
)]
pub fanout: usize,
#[arg(short = 'l', help = "Remote username")]
pub user: Option<String>,
#[arg(short = 't', help = "Connect timeout (seconds)")]
pub connect_timeout: Option<u64>,
#[arg(short = 'u', help = "Command timeout (seconds)")]
pub command_timeout: Option<u64>,
#[arg(short = 'N', help = "Disable hostname prefix")]
pub no_prefix: bool,
#[arg(short = 'b', help = "Batch mode")]
pub batch: bool,
#[arg(short = 'k', help = "Fail fast (stop on first failure)")]
pub fail_fast: bool,
#[arg(short = 'q', help = "Query mode (show hosts and exit)")]
pub query: bool,
#[arg(short = 'S', help = "Return largest exit code from any node")]
pub any_failure: bool,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub command: Vec<String>,
}
pub const PDSH_COMPAT_ENV_VAR: &str = "BSSH_PDSH_COMPAT";
pub fn is_pdsh_compat_mode() -> bool {
if let Ok(value) = std::env::var(PDSH_COMPAT_ENV_VAR) {
let value_lower = value.to_lowercase();
if value_lower == "1" || value_lower == "true" {
return true;
}
}
if let Some(arg0) = std::env::args().next() {
let binary_name = Path::new(&arg0)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if binary_name == "pdsh" || binary_name.starts_with("pdsh.") {
return true;
}
}
false
}
pub fn has_pdsh_compat_flag(args: &[String]) -> bool {
args.iter().any(|arg| arg == "--pdsh-compat")
}
pub fn remove_pdsh_compat_flag(args: &[String]) -> Vec<String> {
args.iter()
.filter(|arg| *arg != "--pdsh-compat")
.cloned()
.collect()
}
impl PdshCli {
pub fn parse_args() -> Self {
PdshCli::parse()
}
pub fn parse_from_args<I, T>(args: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
PdshCli::parse_from(args)
}
pub fn is_query_mode(&self) -> bool {
self.query
}
pub fn has_command(&self) -> bool {
!self.command.is_empty()
}
pub fn get_command(&self) -> String {
self.command.join(" ")
}
pub fn to_bssh_cli(&self) -> super::Cli {
use std::path::PathBuf;
super::Cli {
hosts: self.hosts.as_ref().map(|h| {
h.split(',')
.map(|s| s.trim().to_string())
.collect::<Vec<_>>()
}),
exclude: self.exclude.as_ref().map(|x| {
x.split(',')
.map(|s| s.trim().to_string())
.collect::<Vec<_>>()
}),
parallel: self.fanout,
user: self.user.clone(),
connect_timeout: self.connect_timeout.unwrap_or(30),
timeout: self.command_timeout,
no_prefix: self.no_prefix,
batch: self.batch,
fail_fast: self.fail_fast,
any_failure: self.any_failure,
command_args: self.command.clone(),
pdsh_compat: true,
destination: None,
command: None,
filter: None,
cluster: None,
config: PathBuf::from("~/.config/bssh/config.yaml"),
identity: None,
use_agent: false,
password: false,
sudo_password: false,
jump_hosts: None,
port: None,
stream: false,
output_dir: None,
verbose: 0,
strict_host_key_checking: "accept-new".to_string(),
require_all_success: false,
check_all_nodes: false,
ssh_options: Vec::new(),
ssh_config: None,
quiet: false,
force_tty: false,
no_tty: false,
no_x11: false,
ipv4: false,
ipv6: false,
query: None,
local_forwards: Vec::new(),
remote_forwards: Vec::new(),
dynamic_forwards: Vec::new(),
server_alive_interval: None,
server_alive_count_max: None,
}
}
}
#[derive(Debug)]
pub struct QueryResult {
pub hosts: Vec<String>,
}
impl QueryResult {
pub fn display(&self) {
for host in &self.hosts {
println!("{host}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pdsh_cli_basic_parsing() {
let args = vec!["pdsh", "-w", "host1,host2", "uptime"];
let cli = PdshCli::parse_from_args(args);
assert_eq!(cli.hosts, Some("host1,host2".to_string()));
assert_eq!(cli.command, vec!["uptime"]);
assert_eq!(cli.fanout, 32); assert!(!cli.no_prefix);
assert!(!cli.batch);
assert!(!cli.fail_fast);
assert!(!cli.query);
}
#[test]
fn test_pdsh_cli_all_options() {
let args = vec![
"pdsh",
"-w",
"host1,host2",
"-x",
"badhost",
"-f",
"10",
"-l",
"admin",
"-t",
"30",
"-u",
"300",
"-N",
"-b",
"-k",
"df",
"-h",
];
let cli = PdshCli::parse_from_args(args);
assert_eq!(cli.hosts, Some("host1,host2".to_string()));
assert_eq!(cli.exclude, Some("badhost".to_string()));
assert_eq!(cli.fanout, 10);
assert_eq!(cli.user, Some("admin".to_string()));
assert_eq!(cli.connect_timeout, Some(30));
assert_eq!(cli.command_timeout, Some(300));
assert!(cli.no_prefix);
assert!(cli.batch);
assert!(cli.fail_fast);
assert_eq!(cli.command, vec!["df", "-h"]);
}
#[test]
fn test_pdsh_cli_query_mode() {
let args = vec!["pdsh", "-w", "hosts", "-q"];
let cli = PdshCli::parse_from_args(args);
assert!(cli.is_query_mode());
assert!(cli.command.is_empty());
}
#[test]
fn test_pdsh_cli_any_failure() {
let args = vec!["pdsh", "-w", "hosts", "-S", "cmd"];
let cli = PdshCli::parse_from_args(args);
assert!(cli.any_failure);
}
#[test]
fn test_has_command() {
let args_with_cmd = vec!["pdsh", "-w", "hosts", "uptime"];
let cli_with_cmd = PdshCli::parse_from_args(args_with_cmd);
assert!(cli_with_cmd.has_command());
let args_without_cmd = vec!["pdsh", "-w", "hosts", "-q"];
let cli_without_cmd = PdshCli::parse_from_args(args_without_cmd);
assert!(!cli_without_cmd.has_command());
}
#[test]
fn test_get_command() {
let args = vec!["pdsh", "-w", "hosts", "echo", "hello", "world"];
let cli = PdshCli::parse_from_args(args);
assert_eq!(cli.get_command(), "echo hello world");
}
#[test]
fn test_remove_pdsh_compat_flag() {
let args = vec![
"bssh".to_string(),
"--pdsh-compat".to_string(),
"-w".to_string(),
"hosts".to_string(),
"cmd".to_string(),
];
let filtered = remove_pdsh_compat_flag(&args);
assert_eq!(
filtered,
vec![
"bssh".to_string(),
"-w".to_string(),
"hosts".to_string(),
"cmd".to_string()
]
);
}
#[test]
fn test_has_pdsh_compat_flag() {
let args_with = vec![
"bssh".to_string(),
"--pdsh-compat".to_string(),
"-w".to_string(),
"hosts".to_string(),
];
assert!(has_pdsh_compat_flag(&args_with));
let args_without = vec!["bssh".to_string(), "-w".to_string(), "hosts".to_string()];
assert!(!has_pdsh_compat_flag(&args_without));
}
#[test]
fn test_to_bssh_cli_basic() {
let args = vec!["pdsh", "-w", "host1,host2", "uptime"];
let pdsh_cli = PdshCli::parse_from_args(args);
let bssh_cli = pdsh_cli.to_bssh_cli();
assert_eq!(
bssh_cli.hosts,
Some(vec!["host1".to_string(), "host2".to_string()])
);
assert_eq!(bssh_cli.command_args, vec!["uptime"]);
assert_eq!(bssh_cli.parallel, 32); assert!(bssh_cli.pdsh_compat);
}
#[test]
fn test_to_bssh_cli_all_options() {
let args = vec![
"pdsh",
"-w",
"host1,host2",
"-x",
"badhost",
"-f",
"10",
"-l",
"admin",
"-t",
"60",
"-u",
"600",
"-N",
"-b",
"-k",
"-S",
"df",
"-h",
];
let pdsh_cli = PdshCli::parse_from_args(args);
let bssh_cli = pdsh_cli.to_bssh_cli();
assert_eq!(
bssh_cli.hosts,
Some(vec!["host1".to_string(), "host2".to_string()])
);
assert_eq!(bssh_cli.exclude, Some(vec!["badhost".to_string()]));
assert_eq!(bssh_cli.parallel, 10);
assert_eq!(bssh_cli.user, Some("admin".to_string()));
assert_eq!(bssh_cli.connect_timeout, 60);
assert_eq!(bssh_cli.timeout, Some(600));
assert!(bssh_cli.no_prefix);
assert!(bssh_cli.batch);
assert!(bssh_cli.fail_fast);
assert!(bssh_cli.any_failure);
assert_eq!(bssh_cli.command_args, vec!["df", "-h"]);
}
#[test]
fn test_to_bssh_cli_defaults() {
let args = vec!["pdsh", "-w", "hosts", "cmd"];
let pdsh_cli = PdshCli::parse_from_args(args);
let bssh_cli = pdsh_cli.to_bssh_cli();
assert_eq!(bssh_cli.connect_timeout, 30);
assert_eq!(bssh_cli.timeout, None);
assert_eq!(bssh_cli.parallel, 32);
assert_eq!(bssh_cli.strict_host_key_checking, "accept-new");
}
#[test]
fn test_to_bssh_cli_host_splitting() {
let args = vec!["pdsh", "-w", "host1, host2 , host3", "cmd"];
let pdsh_cli = PdshCli::parse_from_args(args);
let bssh_cli = pdsh_cli.to_bssh_cli();
assert_eq!(
bssh_cli.hosts,
Some(vec![
"host1".to_string(),
"host2".to_string(),
"host3".to_string()
])
);
}
}