use clap::Parser;
#[derive(Parser, Debug)]
#[command(
name = "rusty-pwgen",
version,
about = "Generate pronounceable or random passwords from the OS CSPRNG.",
long_about = "A Rust port of Theodore Ts'o's pwgen. Default mode generates \
pronounceable passwords via a phoneme algorithm; `-s` mode \
generates uniform-random passwords via the OS CSPRNG (`OsRng`)."
)]
pub struct Cli {
#[arg(
short = 'c',
long = "capitalize",
conflicts_with = "no_capitalize",
overrides_with = "capitalize"
)]
pub capitalize: bool,
#[arg(short = 'A', long = "no-capitalize", overrides_with = "no_capitalize")]
pub no_capitalize: bool,
#[arg(
short = 'n',
long = "numerals",
conflicts_with = "no_numerals",
overrides_with = "numerals"
)]
pub numerals: bool,
#[arg(short = '0', long = "no-numerals", overrides_with = "no_numerals")]
pub no_numerals: bool,
#[arg(short = 'y', long = "symbols")]
pub symbols: bool,
#[arg(short = 's', long = "secure")]
pub secure: bool,
#[arg(short = 'B', long = "ambiguous")]
pub ambiguous_filter: bool,
#[arg(short = 'v', long = "no-vowels")]
pub no_vowels: bool,
#[arg(short = '1', conflicts_with = "columnar")]
pub one_column: bool,
#[arg(short = 'C')]
pub columnar: bool,
#[arg(short = 'N', long = "num-passwords")]
pub num_passwords: Option<usize>,
#[arg(short = 'r', long = "remove-chars")]
pub remove_chars: Option<String>,
#[arg(short = 'H', long = "sha1")]
pub sha1: Option<String>,
#[arg(long, conflicts_with = "no_strict")]
pub strict: bool,
#[arg(long = "no-strict")]
pub no_strict: bool,
#[arg(default_value_t = 8)]
pub length: usize,
pub count: Option<usize>,
#[command(subcommand)]
pub command: Option<Subcommand>,
}
#[derive(clap::Subcommand, Debug)]
pub enum Subcommand {
Completions { shell: clap_complete::Shell },
}
impl Cli {
pub fn capitalize_effective(&self) -> bool {
!self.no_capitalize
}
pub fn numerals_effective(&self) -> bool {
!self.no_numerals
}
pub fn resolved_count(&self, is_tty: bool) -> usize {
if let Some(n) = self.num_passwords {
return n;
}
if let Some(n) = self.count {
return n;
}
if is_tty {
let cols_per_row = crate::output::columns() / (self.length + 1).max(1);
cols_per_row.max(1) * crate::DEFAULT_TTY_ROWS
} else {
crate::DEFAULT_COUNT_PIPED
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_compiles() {
let cmd = Cli::command();
assert_eq!(cmd.get_name(), "rusty-pwgen");
}
#[test]
fn parse_no_args_defaults() {
let cli = Cli::try_parse_from(["rusty-pwgen"]).unwrap();
assert_eq!(cli.length, 8);
assert!(cli.count.is_none());
assert!(!cli.secure);
}
#[test]
fn parse_positional_length_and_count() {
let cli = Cli::try_parse_from(["rusty-pwgen", "12", "5"]).unwrap();
assert_eq!(cli.length, 12);
assert_eq!(cli.count, Some(5));
}
#[test]
fn parse_n_flag_wins_over_positional() {
let cli = Cli::try_parse_from(["rusty-pwgen", "-N", "3", "12", "10"]).unwrap();
assert_eq!(cli.length, 12);
assert_eq!(cli.count, Some(10));
assert_eq!(cli.num_passwords, Some(3));
assert_eq!(cli.resolved_count(false), 3);
}
#[test]
fn parse_secure_flag() {
let cli = Cli::try_parse_from(["rusty-pwgen", "-s", "16"]).unwrap();
assert!(cli.secure);
assert_eq!(cli.length, 16);
}
#[test]
fn parse_conflicting_caps_flags_rejected() {
let result = Cli::try_parse_from(["rusty-pwgen", "-c", "-A"]);
assert!(result.is_err());
}
#[test]
fn parse_conflicting_one_vs_columnar_rejected() {
let result = Cli::try_parse_from(["rusty-pwgen", "-1", "-C"]);
assert!(result.is_err());
}
#[test]
fn parse_strict_conflicts_with_no_strict() {
let result = Cli::try_parse_from(["rusty-pwgen", "--strict", "--no-strict"]);
assert!(result.is_err());
}
}