diff --git a/Cargo.lock b/Cargo.lock index 8cc3d14..8bf121f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a2d6eec27fce550d708b2be5d798797e5a55b246b323ef36924a0001996352" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.4" @@ -163,6 +172,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "clap_mangen" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -402,6 +421,8 @@ name = "phraze" version = "0.3.12" dependencies = [ "clap", + "clap_complete", + "clap_mangen", "criterion", "include-lines", "rand", @@ -539,6 +560,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "roff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" + [[package]] name = "rustix" version = "0.38.20" diff --git a/Cargo.toml b/Cargo.toml index 3750fd6..27b3b43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,11 @@ clap = { version = "4.5.4", features = ["derive"] } unicode-normalization = "0.1.23" include-lines = "1.1.2" +[build-dependencies] +clap = { version = "4.5.4", features = ["derive"] } +clap_complete = "4.5.2" +clap_mangen = "0.2.20" + [dev-dependencies] criterion = "0.5.1" diff --git a/benches/generate_passphrase.rs b/benches/generate_passphrase.rs index b850e64..0bd06d7 100644 --- a/benches/generate_passphrase.rs +++ b/benches/generate_passphrase.rs @@ -1,4 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion}; +use phraze::cli::ListChoice; use phraze::*; fn criterion_benchmark(c: &mut Criterion) { diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..4e100f1 --- /dev/null +++ b/build.rs @@ -0,0 +1,40 @@ +#[path = "src/cli.rs"] +mod cli; + +use clap::Command; +use clap::CommandFactory; +use clap_complete::generate_to; +use clap_complete::Shell::{Bash, Fish, Zsh}; +use clap_mangen::Man; +use cli::Args; +use std::fs; +use std::path::PathBuf; + +static NAME: &str = "phraze"; + +fn generate_man_pages(cmd: Command) { + let man_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/man"); + let mut buffer = Vec::default(); + + Man::new(cmd.clone()).render(&mut buffer).unwrap(); + fs::create_dir_all(&man_dir).unwrap(); + fs::write(man_dir.join(NAME.to_owned() + ".1"), buffer).unwrap(); +} + +fn generate_shell_completions(mut cmd: Command) { + let comp_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/completions"); + + fs::create_dir_all(&comp_dir).unwrap(); + + for shell in [Bash, Fish, Zsh] { + generate_to(shell, &mut cmd, NAME, &comp_dir).unwrap(); + } +} + +fn main() { + let mut cmd = Args::command(); + cmd.set_bin_name(NAME); + + generate_man_pages(cmd.clone()); + generate_shell_completions(cmd); +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..1cba470 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,115 @@ +use clap::Parser; +use std::path::PathBuf; + +/// This enum, `ListChoice`, represents all of the "built-in" word lists that Phraze can use. +#[derive(Clone, Debug, Copy)] +pub enum ListChoice { + Long, + Medium, + Eff, + Mnemonicode, + Effshort, + Qwerty, + Alpha, +} + +/// Generate random passphrases +#[derive(Parser, Debug)] +#[clap(version, name = "phraze")] +pub struct Args { + /// Strengthen your passphrase the easy way: Each -S flag increases minimum entropy by 20 bits (above the default of + /// 80 bits). + #[clap(short = 'S', long = "strength", conflicts_with = "number_of_words", conflicts_with = "minimum_entropy", action = clap::ArgAction::Count)] + pub strength_count: u8, + + /// Set minimum amount of entropy in bits for generated passphrase. If neither minimum_entropy or + /// number_of_words is specified, Phraze will default to an 80-bit minimum. + #[clap( + short = 'e', + long = "minimum-entropy", + conflicts_with = "number_of_words", + conflicts_with = "strength_count" + )] + pub minimum_entropy: Option, + + /// Set exactly how many words to use in generated passphrase. If neither number_of_words or + /// minimum_entropy is specified, Phraze will default to an 80-bit minimum. + #[clap( + short = 'w', + long = "words", + conflicts_with = "minimum_entropy", + conflicts_with = "strength_count" + )] + pub number_of_words: Option, + + /// Number of passphrases to generate + #[clap(short = 'n', long = "passphrases", default_value = "1")] + pub n_passphrases: usize, + + /// Word separator. Can accept single quotes around the separator. To not use a separator, + /// use empty single quotes: ''. + /// + /// There are special values that will trigger generated separators: + /// + /// _n: separators will be random numbers + /// + /// _s: separators will be random symbols + /// + /// _b: separators will be a mix of random numbers and symbols + #[clap(short = 's', long = "sep", default_value = "-")] + pub separator: String, + + /// Choose a word list to use. + /// + /// Options: + /// + /// m: Orchard Street Medium List (8,192 words) [DEFAULT] + /// + /// l: Orchard Street Long List (17,576 words) + /// + /// e: EFF long list (7,776 words) + /// + /// n: Mnemonicode list (1,633 words). Good if you know you're going to be speaking + /// passphrases out loud. + /// + /// s: EFF short list (1,296 words) + /// + /// q: Orchard Street QWERTY list (1,296 words). Optimized to minimize travel + /// distance on QWERTY keyboard layout. + /// + /// a: Orchard Street Alpha list (1,296 words). Optimized to minimize travel + /// distance on alphabetical keyboard layout. + #[clap(short = 'l', long = "list", value_parser=parse_list_choice, default_value="m")] + pub list_choice: ListChoice, + + /// Provide a text file with a list of words to randomly generate passphrase + /// from. Should be a text file with one word per line. + #[clap(short = 'c', long = "custom-list", conflicts_with = "list_choice")] + pub custom_list_file_path: Option, + + /// Use Title Case for words in generated passphrase + #[clap(short = 't', long = "title-case")] + pub title_case: bool, + + /// Print estimated entropy of generated passphrase, in bits, along with + /// the passphrase itself + #[clap(short = 'v', long = "verbose")] + pub verbose: bool, +} + +/// Convert list_choice string slice into a ListChoice enum. Clap calls this function. +fn parse_list_choice(list_choice: &str) -> Result { + match list_choice.to_lowercase().as_ref() { + "l" => Ok(ListChoice::Long), + "m" => Ok(ListChoice::Medium), + "e" => Ok(ListChoice::Eff), + "n" => Ok(ListChoice::Mnemonicode), + "s" => Ok(ListChoice::Effshort), + "q" => Ok(ListChoice::Qwerty), + "a" => Ok(ListChoice::Alpha), + _ => Err(format!( + "Inputted list choice '{}' doesn't correspond to an available word list", + list_choice + )), + } +} diff --git a/src/lib.rs b/src/lib.rs index a19bc36..fcaf268 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,23 +1,13 @@ +pub mod cli; pub mod file_reader; pub mod separators; pub mod unicode_normalization_check; +use crate::cli::ListChoice; use crate::separators::make_separator; use include_lines::include_lines; use rand::{seq::SliceRandom, thread_rng, Rng}; -/// This enum, `ListChoice`, represents all of the "built-in" word lists that Phraze can use. -#[derive(Clone, Debug, Copy)] -pub enum ListChoice { - Long, - Medium, - Eff, - Mnemonicode, - Effshort, - Qwerty, - Alpha, -} - /// Given user's inputs, figure out how many words the generated passphrase will need. If user /// specified an exact `number_of_words`, just return that `number_of_words`. If user is using a /// strength_count, do the necessary math. If user specified a `minimum_entropy`, we need to do diff --git a/src/main.rs b/src/main.rs index de19740..4597bcb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,91 +1,7 @@ +use crate::cli::Args; use crate::file_reader::read_in_custom_list; use clap::Parser; use phraze::*; -use std::path::PathBuf; - -/// Generate random passphrases -#[derive(Parser, Debug)] -#[clap(version, name = "phraze")] -struct Args { - /// Strengthen your passphrase the easy way: Each -S flag increases minimum entropy by 20 bits (above the default of - /// 80 bits). - #[clap(short = 'S', long = "strength", conflicts_with = "number_of_words", conflicts_with = "minimum_entropy", action = clap::ArgAction::Count)] - strength_count: u8, - - /// Set minimum amount of entropy in bits for generated passphrase. If neither minimum_entropy or - /// number_of_words is specified, Phraze will default to an 80-bit minimum. - #[clap( - short = 'e', - long = "minimum-entropy", - conflicts_with = "number_of_words", - conflicts_with = "strength_count" - )] - minimum_entropy: Option, - - /// Set exactly how many words to use in generated passphrase. If neither number_of_words or - /// minimum_entropy is specified, Phraze will default to an 80-bit minimum. - #[clap( - short = 'w', - long = "words", - conflicts_with = "minimum_entropy", - conflicts_with = "strength_count" - )] - number_of_words: Option, - - /// Number of passphrases to generate - #[clap(short = 'n', long = "passphrases", default_value = "1")] - n_passphrases: usize, - - /// Word separator. Can accept single quotes around the separator. To not use a separator, - /// use empty single quotes: ''. - /// - /// There are special values that will trigger generated separators: - /// - /// _n: separators will be random numbers - /// - /// _s: separators will be random symbols - /// - /// _b: separators will be a mix of random numbers and symbols - #[clap(short = 's', long = "sep", default_value = "-")] - separator: String, - - /// Choose a word list to use. - /// - /// Options: - /// - /// m: Orchard Street Medium List (8,192 words) [DEFAULT] - /// - /// l: Orchard Street Long List (17,576 words) - /// - /// e: EFF long list (7,776 words) - /// - /// n: Mnemonicode list (1,633 words). Good if you know you're going to be speaking - /// passphrases out loud. - /// - /// s: EFF short list (1,296 words) - /// - /// q: Orchard Street QWERTY list (1,296 words). Optimized to minimize travel - /// distance on QWERTY keyboard layout. - /// - /// a: Orchard Street Alpha list (1,296 words). Optimized to minimize travel - /// distance on alphabetical keyboard layout. - #[clap(short = 'l', long = "list", value_parser=parse_list_choice, default_value="m")] - list_choice: ListChoice, - - /// Provide a text file with a list of words to randomly generate passphrase - /// from. Should be a text file with one word per line. - #[clap(short = 'c', long = "custom-list", conflicts_with = "list_choice")] - custom_list_file_path: Option, - - /// Use Title Case for words in generated passphrase - #[clap(short = 't', long = "title-case")] - title_case: bool, - - /// Print estimated entropy of generated passphrase, in bits, along with - /// the passphrase itself - #[clap(short = 'v', long = "verbose")] - verbose: bool, -} fn main() -> Result<(), String> { let opt = Args::parse(); @@ -139,20 +55,3 @@ fn generate_passphrases + std::fmt::Display>(opt: &Args, word_list println!("{}", passphrase); } } - -/// Convert list_choice string slice into a ListChoice enum. Clap calls this function. -fn parse_list_choice(list_choice: &str) -> Result { - match list_choice.to_lowercase().as_ref() { - "l" => Ok(ListChoice::Long), - "m" => Ok(ListChoice::Medium), - "e" => Ok(ListChoice::Eff), - "n" => Ok(ListChoice::Mnemonicode), - "s" => Ok(ListChoice::Effshort), - "q" => Ok(ListChoice::Qwerty), - "a" => Ok(ListChoice::Alpha), - _ => Err(format!( - "Inputted list choice '{}' doesn't correspond to an available word list", - list_choice - )), - } -} diff --git a/tests/list_reading_tests.rs b/tests/list_reading_tests.rs index b60e07a..5d11c60 100644 --- a/tests/list_reading_tests.rs +++ b/tests/list_reading_tests.rs @@ -1,4 +1,5 @@ mod minimum_entropy_tests { + use phraze::cli::ListChoice; use phraze::*; #[test]