Add help messages to CLI flags and options

type: documentation
This commit is contained in:
Casey Rodarmor 2020-01-24 14:17:31 -08:00
parent b83d8d7ef5
commit 70e0091ec0
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
8 changed files with 175 additions and 49 deletions

View File

@ -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;

View File

@ -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.";

View File

@ -110,7 +110,16 @@ mod tests {
#[test]
fn error_message_on_stdout() {
let mut env = testing::env(
["torrent", "create", "--input", "foo", "--announce", "bar"]
[
"torrent",
"create",
"--input",
"foo",
"--announce",
"udp:bar.com",
"--announce-tier",
"foo",
]
.iter()
.cloned(),
);

View File

@ -4,7 +4,7 @@ use crate::common::*;
pub struct Metainfo {
pub announce: String,
#[serde(rename = "announce list")]
pub announce_list: Vec<Vec<String>>,
pub announce_list: Option<Vec<Vec<String>>>,
pub comment: Option<String>,
#[serde(rename = "created by")]
pub created_by: Option<String>,

View File

@ -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)]

View File

@ -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),

View File

@ -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<String>,
#[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<String>,
#[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<String>,
#[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<String>,
#[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<PathBuf>,
#[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::<Vec<String>>();
for tier in &self.announce_tiers {
let tier = tier.split(',').map(str::to_string).collect::<Vec<String>>();
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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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()],
])
);
}

View File

@ -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<u64>,
#[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<Regex>,
#[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);