From 70e0091ec03ca0014974ebf98673f3e37a1fea75 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 24 Jan 2020 14:17:31 -0800 Subject: [PATCH] Add `help` messages to CLI flags and options type: documentation --- src/common.rs | 5 +- src/consts.rs | 4 ++ src/env.rs | 15 ++++- src/metainfo.rs | 2 +- src/opt.rs | 13 +++- src/torrent.rs | 5 ++ src/torrent/create.rs | 142 +++++++++++++++++++++++++++++++----------- src/torrent/stats.rs | 38 +++++++++-- 8 files changed, 175 insertions(+), 49 deletions(-) diff --git a/src/common.rs b/src/common.rs index 8d769e3..f38cd49 100644 --- a/src/common.rs +++ b/src/common.rs @@ -24,7 +24,10 @@ pub(crate) use serde::{Deserialize, Serialize}; pub(crate) use sha1::Sha1; pub(crate) use snafu::{ResultExt, Snafu}; pub(crate) use static_assertions::const_assert; -pub(crate) use structopt::StructOpt; +pub(crate) use structopt::{ + clap::{AppSettings, ArgSettings}, + StructOpt, +}; pub(crate) use url::Url; pub(crate) use walkdir::WalkDir; diff --git a/src/consts.rs b/src/consts.rs index 63d520f..cdedfdc 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -21,3 +21,7 @@ pub(crate) const ABOUT: &str = concat!( pub(crate) const VERSION: &str = concat!("v", env!("CARGO_PKG_VERSION")); pub(crate) const AUTHOR: &str = env!("CARGO_PKG_AUTHORS"); + +pub(crate) const HELP_MESSAGE: &str = "Print help message."; + +pub(crate) const VERSION_MESSAGE: &str = "Print version number."; diff --git a/src/env.rs b/src/env.rs index bbacbb5..d4b6ae2 100644 --- a/src/env.rs +++ b/src/env.rs @@ -110,9 +110,18 @@ mod tests { #[test] fn error_message_on_stdout() { let mut env = testing::env( - ["torrent", "create", "--input", "foo", "--announce", "bar"] - .iter() - .cloned(), + [ + "torrent", + "create", + "--input", + "foo", + "--announce", + "udp:bar.com", + "--announce-tier", + "foo", + ] + .iter() + .cloned(), ); fs::write(env.resolve("foo"), "").unwrap(); env.status().ok(); diff --git a/src/metainfo.rs b/src/metainfo.rs index ff0922c..8e59428 100644 --- a/src/metainfo.rs +++ b/src/metainfo.rs @@ -4,7 +4,7 @@ use crate::common::*; pub struct Metainfo { pub announce: String, #[serde(rename = "announce list")] - pub announce_list: Vec>, + pub announce_list: Option>>, pub comment: Option, #[serde(rename = "created by")] pub created_by: Option, diff --git a/src/opt.rs b/src/opt.rs index ac95b77..73c4fad 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -1,23 +1,30 @@ use crate::common::*; -use structopt::clap::{AppSettings, ArgSettings}; - #[derive(StructOpt)] #[structopt( about(consts::ABOUT), version(consts::VERSION), author(consts::AUTHOR), + help_message(consts::HELP_MESSAGE), + version_message(consts::VERSION_MESSAGE), global_setting(AppSettings::ColoredHelp), global_setting(AppSettings::ColorAuto) )] pub(crate) struct Opt { - #[structopt(long = "unstable", short = "u")] + #[structopt( + long = "unstable", + short = "u", + help = "Enable unstable features.", + long_help = "Enable unstable features. To avoid premature stabilization and excessive version churn, unstable features are unavailable unless this flag is set. Unstable features are not bound by semantic versioning stability guarantees, and may be changed or removed at any time." + )] unstable: bool, #[structopt( long = "color", default_value = use_color::AUTO, set = ArgSettings::CaseInsensitive, possible_values = use_color::VALUES, + help = "Print colorful output.", + long_help = "Print colorful output. When `auto`, the default, colored output is only enabled if imdl detects that it is connected to a terminal, the `NO_COLOR` environment variable is not set, and the `TERM` environment variable is not set with a value of `dumb`.", )] pub(crate) use_color: UseColor, #[structopt(subcommand)] diff --git a/src/torrent.rs b/src/torrent.rs index 63f0c59..8087f1b 100644 --- a/src/torrent.rs +++ b/src/torrent.rs @@ -4,6 +4,11 @@ mod create; mod stats; #[derive(StructOpt)] +#[structopt( + help_message(consts::HELP_MESSAGE), + version_message(consts::VERSION_MESSAGE), + about("Subcommands related to the BitTorrent protocol.") +)] pub(crate) enum Torrent { Create(torrent::create::Create), Stats(torrent::stats::Stats), diff --git a/src/torrent/create.rs b/src/torrent/create.rs index b1252f6..0c9dafc 100644 --- a/src/torrent/create.rs +++ b/src/torrent/create.rs @@ -1,26 +1,92 @@ use crate::common::*; #[derive(StructOpt)] +#[structopt( + help_message(consts::HELP_MESSAGE), + version_message(consts::VERSION_MESSAGE), + about("Create a `.torrent` file.") +)] pub(crate) struct Create { - #[structopt(name = "ANNOUNCE", long = "announce", required(true))] - announce: Vec, - #[structopt(name = "COMMENT", long = "comment")] + #[structopt( + name = "ANNOUNCE", + long = "announce", + required(true), + help = "Use `ANNOUNCE` as the primary tracker announce URL.", + long_help = "Use `ANNOUNCE` as the primary tracker announce URL. To supply multiple announce URLs, also use `--announce-tier`." + )] + announce: Url, + #[structopt( + long = "announce-tier", + name = "ANNOUNCE-TIER", + help = "Add `ANNOUNCE-TIER` to list of tracker announce tiers.", + long_help = "\ +Add `ANNOUNCE-TIER` to list of tracker announce tiers. Each instance adds a new tier. To add multiple trackers to a given tier, separate their announce URLs with commas: + +`--announce-tier udp://example.com:80/announce,https://example.net:443/announce` + +Announce tiers are stored in the `announce-list` key of the top-level metainfo dictionary as a list of lists of strings, as defined by BEP 12: Multitracker Metadata Extension. + +Note: Many BitTorrent clients do not implement the behavior described in BEP 12. See the discussion here for more details: https://github.com/bittorrent/bittorrent.org/issues/82" + )] + announce_tiers: Vec, + #[structopt( + name = "COMMENT", + long = "comment", + help = "Include `COMMENT` in generated `.torrent` file.", + long_help = "Include `COMMENT` in generated `.torrent` file. Stored under `comment` key of top-level metainfo dictionary." + )] comment: Option, - #[structopt(name = "INPUT", long = "input")] + #[structopt( + name = "INPUT", + long = "input", + help = "Read torrent contents from `INPUT`.", + long_help = "Read torrent contents from `INPUT`. If `INPUT` is a file, torrent will be a single-file torrent, otherwise if `INPUT` is a directory, torrent will be a multi-file torrent." + )] input: PathBuf, - #[structopt(name = "MD5SUM", long = "md5sum")] + #[structopt( + name = "MD5SUM", + long = "md5sum", + help = "Include MD5 checksum of each file in the torrent. N.B. MD5 is cryptographically broken and only suitable for safeguarding against accidental corruption.", + long_help = "Include MD5 checksum of each file in the torrent. N.B. MD5 is cryptographically broken and only suitable for checking for accidental corruption." + )] md5sum: bool, - #[structopt(name = "NAME", long = "name")] + #[structopt( + name = "NAME", + long = "name", + help = "Set name of torrent to `NAME`. Defaults to the filename of `--input`." + )] name: Option, - #[structopt(name = "NO-CREATED-BY", long = "no-created-by")] + #[structopt( + name = "NO-CREATED-BY", + long = "no-created-by", + help = "Do not populate `created by` key of generated torrent with imdl version information." + )] no_created_by: bool, - #[structopt(name = "NO-CREATION-DATE", long = "no-creation-date")] + #[structopt( + name = "NO-CREATION-DATE", + long = "no-creation-date", + help = "Do not populate `creation date` key of generated torrent with current time." + )] no_creation_date: bool, - #[structopt(name = "OUTPUT", long = "output")] + #[structopt( + name = "OUTPUT", + long = "output", + help = "Save `.torrent` file to `OUTPUT`. Defaults to `$INPUT.torrent`." + )] output: Option, - #[structopt(name = "PIECE-LENGTH", long = "piece-length", default_value = "524288")] + #[structopt( + name = "PIECE-LENGTH", + long = "piece-length", + default_value = "524288", + help = "Set piece length to `PIECE-LENGTH` bytes." + )] piece_length: u32, - #[structopt(name = "PRIVATE", long = "private")] + #[structopt( + name = "PRIVATE", + long = "private", + help = "Set the `private` flag.", + long_help = "Set the `private` flag. Torrent clients that understand the flag and participate in the swarm of a torrent with the flag set will only announce themselves to the announce URLs included in the torrent, and will not use other peer discovery mechanisms, such as the DHT or local peer discovery. See BEP 27: Private Torrents for more information." + )] private: bool, } @@ -29,11 +95,8 @@ impl Create { let input = env.resolve(&self.input); let mut announce_list = Vec::new(); - for announce in &self.announce { - let tier = announce - .split(',') - .map(str::to_string) - .collect::>(); + for tier in &self.announce_tiers { + let tier = tier.split(',').map(str::to_string).collect::>(); tier .iter() @@ -44,12 +107,6 @@ impl Create { announce_list.push(tier); } - let announce = if let Some(primary) = announce_list.first().and_then(|tier| tier.first()) { - primary.clone() - } else { - return Err(Error::AnnounceEmpty); - }; - let filename = input.file_name().ok_or_else(|| Error::FilenameExtract { path: input.clone(), })?; @@ -106,8 +163,12 @@ impl Create { let metainfo = Metainfo { comment: self.comment, encoding: consts::ENCODING_UTF8.to_string(), - announce, - announce_list, + announce: self.announce.to_string(), + announce_list: if announce_list.is_empty() { + None + } else { + Some(announce_list) + }, creation_date, created_by, info, @@ -180,7 +241,7 @@ mod tests { fn tracker_flag_must_be_url() { let mut env = environment(&["--input", "foo", "--announce", "bar"]); fs::write(env.resolve("foo"), "").unwrap(); - assert_matches!(env.run(), Err(Error::AnnounceUrlParse { .. })); + assert_matches!(env.run(), Err(Error::Clap { .. })); } #[test] @@ -191,22 +252,29 @@ mod tests { let torrent = env.resolve("foo.torrent"); let bytes = fs::read(torrent).unwrap(); let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); - assert_eq!(metainfo.announce, "http://bar"); - assert_eq!(metainfo.announce_list, vec![vec!["http://bar"]]); + assert_eq!(metainfo.announce, "http://bar/"); + assert!(metainfo.announce_list.is_none()); } #[test] fn announce_single_tier() { - let mut env = environment(&["--input", "foo", "--announce", "http://bar,http://baz"]); + let mut env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--announce-tier", + "http://bar,http://baz", + ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); let torrent = env.resolve("foo.torrent"); let bytes = fs::read(torrent).unwrap(); let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); - assert_eq!(metainfo.announce, "http://bar"); + assert_eq!(metainfo.announce, "http://bar/"); assert_eq!( metainfo.announce_list, - vec![vec!["http://bar", "http://baz"]] + Some(vec![vec!["http://bar".into(), "http://baz".into()]]), ); } @@ -216,8 +284,10 @@ mod tests { "--input", "foo", "--announce", + "http://bar", + "--announce-tier", "http://bar,http://baz", - "--announce", + "--announce-tier", "http://abc,http://xyz", ]); fs::write(env.resolve("foo"), "").unwrap(); @@ -225,13 +295,13 @@ mod tests { let torrent = env.resolve("foo.torrent"); let bytes = fs::read(torrent).unwrap(); let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); - assert_eq!(metainfo.announce, "http://bar"); + assert_eq!(metainfo.announce, "http://bar/"); assert_eq!( metainfo.announce_list, - vec![ - vec!["http://bar", "http://baz"], - vec!["http://abc", "http://xyz"], - ] + Some(vec![ + vec!["http://bar".into(), "http://baz".into()], + vec!["http://abc".into(), "http://xyz".into()], + ]) ); } diff --git a/src/torrent/stats.rs b/src/torrent/stats.rs index 30634fe..ba2de34 100644 --- a/src/torrent/stats.rs +++ b/src/torrent/stats.rs @@ -1,14 +1,42 @@ use crate::common::*; #[derive(StructOpt)] +#[structopt( + help_message(consts::HELP_MESSAGE), + version_message(consts::VERSION_MESSAGE), + about("Show statistics about a collection of `.torrent` files.") +)] pub(crate) struct Stats { - #[structopt(long = "limit", short = "l")] + #[structopt( + name = "COUNT", + long = "limit", + short = "l", + help = "Stop after processing the first `COUNT` torrents.", + long_help = "Stop after processing the first `COUNT` torrents. Useful when processing large collections of `.torrent` files." + )] limit: Option, - #[structopt(long = "extract-pattern", short = "e")] + #[structopt( + name = "REGEX", + long = "extract-pattern", + short = "e", + help = "Extract and display values from key paths that match `REGEX`.", + long_help = "\ +Extract and display values under key paths that match `REGEX`. Subkeys of a bencodeded dictionary are delimited by `/`, and values of a bencoded list are delmited by `*`. For example, given the following bencoded dictionary `{\"foo\": [{\"bar\": {\"baz\": 2}}]}`, the value `2`'s key path will be `foo*bar/baz`. The value `2` would be displayed if any of `bar`, `foo[*]bar/baz`, or `foo.*baz` were passed to `--extract-pattern." + )] extract_patterns: Vec, - #[structopt(name = "INPUT", long = "input", short = "i")] + #[structopt( + name = "INPUT", + long = "input", + short = "i", + help = "Search `INPUT` for torrents.", + long_help = "Search `INPUT` for torrents. May be a directory to search or a single torrent file." + )] input: PathBuf, - #[structopt(long = "print", short = "p")] + #[structopt( + long = "print", + short = "p", + help = "Pretty print the contents of each torrent as it is processed." + )] print: bool, } @@ -139,7 +167,7 @@ impl Extractor { }; if self.print { - eprintln!("{}: {}", path.display(), value); + eprintln!("{}:\n{}", path.display(), value); } self.extract(&value);