diff --git a/b3sum/Cargo.lock b/b3sum/Cargo.lock index 2300d3b..d9043f1 100644 --- a/b3sum/Cargo.lock +++ b/b3sum/Cargo.lock @@ -75,6 +75,7 @@ dependencies = [ "anyhow", "blake3", "clap", + "clap_mangen", "duct", "hex", "rayon", @@ -154,6 +155,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "clap_mangen" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -318,6 +329,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "roff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" + [[package]] name = "rustix" version = "0.38.31" diff --git a/b3sum/Cargo.toml b/b3sum/Cargo.toml index 812ed22..4bc7649 100644 --- a/b3sum/Cargo.toml +++ b/b3sum/Cargo.toml @@ -24,3 +24,8 @@ wild = "2.0.3" [dev-dependencies] duct = "0.13.3" tempfile = "3.1.0" + +[build-dependencies] +blake3 = { version = "1", path = ".." } +clap = { version = "4.0.8", features = ["derive"] } +clap_mangen = "0.2.20" diff --git a/b3sum/build.rs b/b3sum/build.rs new file mode 100644 index 0000000..844edc1 --- /dev/null +++ b/b3sum/build.rs @@ -0,0 +1,59 @@ +use clap::CommandFactory; + +include!("src/cli.rs"); + +fn generate_man_page(out_dir: &std::path::Path) -> std::io::Result<()> { + let command = Inner::command(); + + let man = clap_mangen::Man::new(command).date("2024-04-24"); + let mut buf = Vec::new(); + man.render_title(&mut buf)?; + + // The NAME section. + let mut roff = clap_mangen::roff::Roff::new(); + roff.control("SH", ["NAME"]); + roff.text([clap_mangen::roff::roman( + "b3sum - compute and check BLAKE3 message digest", + )]); + roff.to_writer(&mut buf)?; + + // The SYNOPSIS section. + let mut roff = clap_mangen::roff::Roff::new(); + roff.control("SH", ["SYNOPSIS"]); + roff.text([ + clap_mangen::roff::bold("b3sum"), + clap_mangen::roff::roman(" ["), + clap_mangen::roff::italic("OPTIONS"), + clap_mangen::roff::roman("] ["), + clap_mangen::roff::italic("FILE"), + clap_mangen::roff::roman("]..."), + ]); + roff.to_writer(&mut buf)?; + + man.render_description_section(&mut buf)?; + man.render_options_section(&mut buf)?; + + // The SEE ALSO section. + let mut roff = clap_mangen::roff::Roff::new(); + roff.control("SH", ["SEE ALSO"]); + roff.text([ + clap_mangen::roff::bold("b2sum"), + clap_mangen::roff::roman("(1), "), + clap_mangen::roff::bold("md5sum"), + clap_mangen::roff::roman("(1)"), + ]); + roff.to_writer(&mut buf)?; + + std::fs::write(out_dir.join("b3sum.1"), buf)?; + Ok(()) +} + +fn main() -> std::io::Result<()> { + println!("cargo:rerun-if-changed=src/cli.rs"); + + let out_dir = std::env::var("OUT_DIR").expect("environment variable `OUT_DIR` not defined"); + let out_dir = std::path::PathBuf::from(out_dir); + + generate_man_page(&out_dir)?; + Ok(()) +} diff --git a/b3sum/src/cli.rs b/b3sum/src/cli.rs new file mode 100644 index 0000000..9a5bf2e --- /dev/null +++ b/b3sum/src/cli.rs @@ -0,0 +1,96 @@ +use clap::Parser; +use std::path::PathBuf; + +const DERIVE_KEY_ARG: &str = "derive_key"; +const KEYED_ARG: &str = "keyed"; +const LENGTH_ARG: &str = "length"; +const NO_NAMES_ARG: &str = "no_names"; +const RAW_ARG: &str = "raw"; +const CHECK_ARG: &str = "check"; + +/// Print or check BLAKE3 checksums. +/// +/// With no FILE, or when FILE is -, read standard input. +#[derive(Parser)] +#[command(version, max_term_width(100))] +pub struct Inner { + /// Files to hash, or checkfiles to check + /// + /// When no file is given, or when - is given, read standard input. + pub file: Vec, + + /// Use the keyed mode, reading the 32-byte key from stdin + #[arg(long, requires("file"))] + pub keyed: bool, + + /// Use the key derivation mode, with the given context string + /// + /// Cannot be used with --keyed. + #[arg(long, value_name("CONTEXT"), conflicts_with(KEYED_ARG))] + pub derive_key: Option, + + /// The number of output bytes, before hex encoding + #[arg( + short, + long, + default_value_t = blake3::OUT_LEN as u64, + value_name("LEN") + )] + pub length: u64, + + /// The starting output byte offset, before hex encoding + #[arg(long, default_value_t = 0, value_name("SEEK"))] + pub seek: u64, + + /// The maximum number of threads to use + /// + /// By default, this is the number of logical cores. If this flag is + /// omitted, or if its value is 0, RAYON_NUM_THREADS is also respected. + #[arg(long, value_name("NUM"))] + pub num_threads: Option, + + /// Disable memory mapping + /// + /// Currently this also disables multithreading. + #[arg(long)] + pub no_mmap: bool, + + /// Omit filenames in the output + #[arg(long)] + pub no_names: bool, + + /// Write raw output bytes to stdout, rather than hex + /// + /// --no-names is implied. In this case, only a single input is allowed. + #[arg(long)] + pub raw: bool, + + /// Read BLAKE3 sums from the [FILE]s and check them + #[arg( + short, + long, + conflicts_with(DERIVE_KEY_ARG), + conflicts_with(KEYED_ARG), + conflicts_with(LENGTH_ARG), + conflicts_with(RAW_ARG), + conflicts_with(NO_NAMES_ARG) + )] + pub check: bool, + + /// Skip printing OK for each checked file + /// + /// Must be used with --check. + #[arg(long, requires(CHECK_ARG))] + pub quiet: bool, +} + +#[cfg(test)] +mod test { + use super::*; + use clap::CommandFactory; + + #[test] + fn test_args() { + Inner::command().debug_assert(); + } +} diff --git a/b3sum/src/main.rs b/b3sum/src/main.rs index 228737f..8e08667 100644 --- a/b3sum/src/main.rs +++ b/b3sum/src/main.rs @@ -6,93 +6,15 @@ use std::io; use std::io::prelude::*; use std::path::{Path, PathBuf}; +mod cli; + #[cfg(test)] mod unit_tests; const NAME: &str = "b3sum"; -const DERIVE_KEY_ARG: &str = "derive_key"; -const KEYED_ARG: &str = "keyed"; -const LENGTH_ARG: &str = "length"; -const NO_NAMES_ARG: &str = "no_names"; -const RAW_ARG: &str = "raw"; -const CHECK_ARG: &str = "check"; - -#[derive(Parser)] -#[command(version, max_term_width(100))] -struct Inner { - /// Files to hash, or checkfiles to check - /// - /// When no file is given, or when - is given, read standard input. - file: Vec, - - /// Use the keyed mode, reading the 32-byte key from stdin - #[arg(long, requires("file"))] - keyed: bool, - - /// Use the key derivation mode, with the given context string - /// - /// Cannot be used with --keyed. - #[arg(long, value_name("CONTEXT"), conflicts_with(KEYED_ARG))] - derive_key: Option, - - /// The number of output bytes, before hex encoding - #[arg( - short, - long, - default_value_t = blake3::OUT_LEN as u64, - value_name("LEN") - )] - length: u64, - - /// The starting output byte offset, before hex encoding - #[arg(long, default_value_t = 0, value_name("SEEK"))] - seek: u64, - - /// The maximum number of threads to use - /// - /// By default, this is the number of logical cores. If this flag is - /// omitted, or if its value is 0, RAYON_NUM_THREADS is also respected. - #[arg(long, value_name("NUM"))] - num_threads: Option, - - /// Disable memory mapping - /// - /// Currently this also disables multithreading. - #[arg(long)] - no_mmap: bool, - - /// Omit filenames in the output - #[arg(long)] - no_names: bool, - - /// Write raw output bytes to stdout, rather than hex - /// - /// --no-names is implied. In this case, only a single input is allowed. - #[arg(long)] - raw: bool, - - /// Read BLAKE3 sums from the [FILE]s and check them - #[arg( - short, - long, - conflicts_with(DERIVE_KEY_ARG), - conflicts_with(KEYED_ARG), - conflicts_with(LENGTH_ARG), - conflicts_with(RAW_ARG), - conflicts_with(NO_NAMES_ARG) - )] - check: bool, - - /// Skip printing OK for each checked file - /// - /// Must be used with --check. - #[arg(long, requires(CHECK_ARG))] - quiet: bool, -} - struct Args { - inner: Inner, + inner: crate::cli::Inner, file_args: Vec, base_hasher: blake3::Hasher, } @@ -101,7 +23,7 @@ impl Args { fn parse() -> Result { // wild::args_os() is equivalent to std::env::args_os() on Unix, // but on Windows it adds support for globbing. - let inner = Inner::parse_from(wild::args_os()); + let inner = crate::cli::Inner::parse_from(wild::args_os()); let file_args = if !inner.file.is_empty() { inner.file.clone() } else { @@ -509,13 +431,3 @@ fn main() -> Result<()> { std::process::exit(if files_failed > 0 { 1 } else { 0 }); }) } - -#[cfg(test)] -mod test { - use clap::CommandFactory; - - #[test] - fn test_args() { - crate::Inner::command().debug_assert(); - } -}