This commit is contained in:
elwrcl committed 2026-09-26 17:09:38 +03:00
commit 6df73cc246
22 files changed
+11922

No files matched your search

+253
View File
@@ -0,0 +1,253 @@
use crate::generator::charset::CharsetOptions;
use crate::generator::passphrase::PassphraseOptions;
use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Parser, Debug)]
#[command(name = "pasx", version, about = "Terminal password generator")]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[command(flatten)]
pub opts: GenArgs,
}
#[derive(Subcommand, Debug)]
pub enum Command {
/// Generate one password, print it, and exit
Gen(GenCmdArgs),
/// Print a shell completion script
Completions {
#[arg(value_enum)]
shell: ShellKind,
},
}
#[derive(Args, Debug)]
pub struct GenCmdArgs {
#[command(flatten)]
pub opts: GenArgs,
/// Also copy the password to the clipboard
#[arg(long)]
pub copy: bool,
}
#[derive(Args, Debug, Clone)]
pub struct GenArgs {
/// Generation mode
#[arg(short = 'm', long, value_enum, default_value_t = Mode::Charset)]
pub mode: Mode,
/// Password length in characters
#[arg(short = 'l', long, default_value_t = 18)]
pub length: usize,
/// Exclude uppercase letters
#[arg(long)]
pub no_upper: bool,
/// Exclude lowercase letters
#[arg(long)]
pub no_lower: bool,
/// Exclude digits
#[arg(long)]
pub no_digits: bool,
/// Exclude symbols
#[arg(long)]
pub no_symbols: bool,
/// Exclude look-alike characters (0 O o l 1 I)
#[arg(long)]
pub exclude_ambiguous: bool,
/// Number of words in passphrase mode
#[arg(short = 'w', long, default_value_t = 5)]
pub words: usize,
/// Separator between words in passphrase mode
#[arg(long, default_value_t = '-')]
pub separator: char,
/// Capitalize each word in passphrase mode
#[arg(long)]
pub capitalize: bool,
/// Append a random digit in passphrase mode
#[arg(long)]
pub add_number: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Charset,
Passphrase,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShellKind {
Nu,
Bash,
Zsh,
Fish,
}
impl GenArgs {
pub fn charset_options(&self) -> CharsetOptions {
CharsetOptions {
length: self.length,
upper: !self.no_upper,
lower: !self.no_lower,
digits: !self.no_digits,
symbols: !self.no_symbols,
exclude_ambiguous: self.exclude_ambiguous,
}
}
pub fn passphrase_options(&self) -> PassphraseOptions {
PassphraseOptions {
words: self.words,
separator: self.separator,
capitalize: self.capitalize,
add_number: self.add_number,
}
}
}
use clap::CommandFactory;
pub fn write_completions(shell: ShellKind, out: &mut impl std::io::Write) -> std::io::Result<()> {
let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
// clap_complete panics internally if the writer it is handed fails, so it
// renders into memory and the fallible write happens here.
let mut script = Vec::new();
match shell {
ShellKind::Nu => {
clap_complete::generate(clap_complete_nushell::Nushell, &mut cmd, name, &mut script)
}
ShellKind::Bash => {
clap_complete::generate(clap_complete::Shell::Bash, &mut cmd, name, &mut script)
}
ShellKind::Zsh => {
clap_complete::generate(clap_complete::Shell::Zsh, &mut cmd, name, &mut script)
}
ShellKind::Fish => {
clap_complete::generate(clap_complete::Shell::Fish, &mut cmd, name, &mut script)
}
}
out.write_all(&script)
}
#[cfg(test)]
mod tests {
use super::*;
use clap::{CommandFactory, Parser};
#[test]
fn argument_definition_is_valid() {
Cli::command().debug_assert();
}
#[test]
fn bare_invocation_has_no_subcommand_and_full_defaults() {
let cli = Cli::parse_from(["pasx"]);
assert!(cli.command.is_none());
assert_eq!(cli.opts.mode, Mode::Charset);
assert_eq!(cli.opts.charset_options(), CharsetOptions::default());
assert_eq!(cli.opts.passphrase_options(), PassphraseOptions::default());
}
#[test]
fn negative_flags_switch_classes_off() {
let cli = Cli::parse_from(["pasx", "--length", "24", "--no-symbols", "--no-upper"]);
let opts = cli.opts.charset_options();
assert_eq!(opts.length, 24);
assert!(!opts.symbols);
assert!(!opts.upper);
assert!(opts.lower);
assert!(opts.digits);
}
#[test]
fn passphrase_flags_reach_the_options() {
let cli = Cli::parse_from([
"pasx",
"--mode",
"passphrase",
"--words",
"7",
"--separator",
".",
"--capitalize",
]);
assert_eq!(cli.opts.mode, Mode::Passphrase);
let opts = cli.opts.passphrase_options();
assert_eq!(opts.words, 7);
assert_eq!(opts.separator, '.');
assert!(opts.capitalize);
assert!(!opts.add_number);
}
#[test]
fn gen_subcommand_takes_the_same_flags_plus_copy() {
let cli = Cli::parse_from(["pasx", "gen", "-l", "32", "--copy"]);
match cli.command {
Some(Command::Gen(args)) => {
assert_eq!(args.opts.charset_options().length, 32);
assert!(args.copy);
}
other => panic!("expected gen subcommand, got {other:?}"),
}
}
#[test]
fn completions_subcommand_parses_every_shell() {
for (arg, expected) in [
("nu", ShellKind::Nu),
("bash", ShellKind::Bash),
("zsh", ShellKind::Zsh),
("fish", ShellKind::Fish),
] {
let cli = Cli::parse_from(["pasx", "completions", arg]);
match cli.command {
Some(Command::Completions { shell }) => assert_eq!(shell, expected),
other => panic!("expected completions subcommand, got {other:?}"),
}
}
}
#[test]
fn exclude_ambiguous_reaches_the_options() {
let cli = Cli::parse_from(["pasx", "--exclude-ambiguous"]);
assert!(cli.opts.charset_options().exclude_ambiguous);
}
#[test]
fn nushell_completions_mention_the_binary_and_its_flags() {
let mut buf = Vec::new();
write_completions(ShellKind::Nu, &mut buf).unwrap();
let script = String::from_utf8(buf).unwrap();
assert!(script.contains("pasx"), "{script}");
assert!(script.contains("--exclude-ambiguous"), "{script}");
assert!(script.contains("pasx gen"), "{script}");
}
#[test]
fn every_shell_produces_a_non_empty_script() {
let shells = [
ShellKind::Nu,
ShellKind::Bash,
ShellKind::Zsh,
ShellKind::Fish,
];
for shell in shells {
let mut buf = Vec::new();
write_completions(shell, &mut buf).unwrap();
assert!(!buf.is_empty(), "{shell:?} produced nothing");
}
}
}