Add help
messages to CLI flags and options
type: documentation
This commit is contained in:
parent
b83d8d7ef5
commit
70e0091ec0
|
@ -24,7 +24,10 @@ pub(crate) use serde::{Deserialize, Serialize};
|
||||||
pub(crate) use sha1::Sha1;
|
pub(crate) use sha1::Sha1;
|
||||||
pub(crate) use snafu::{ResultExt, Snafu};
|
pub(crate) use snafu::{ResultExt, Snafu};
|
||||||
pub(crate) use static_assertions::const_assert;
|
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 url::Url;
|
||||||
pub(crate) use walkdir::WalkDir;
|
pub(crate) use walkdir::WalkDir;
|
||||||
|
|
||||||
|
|
|
@ -21,3 +21,7 @@ pub(crate) const ABOUT: &str = concat!(
|
||||||
pub(crate) const VERSION: &str = concat!("v", env!("CARGO_PKG_VERSION"));
|
pub(crate) const VERSION: &str = concat!("v", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
pub(crate) const AUTHOR: &str = env!("CARGO_PKG_AUTHORS");
|
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.";
|
||||||
|
|
15
src/env.rs
15
src/env.rs
|
@ -110,9 +110,18 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn error_message_on_stdout() {
|
fn error_message_on_stdout() {
|
||||||
let mut env = testing::env(
|
let mut env = testing::env(
|
||||||
["torrent", "create", "--input", "foo", "--announce", "bar"]
|
[
|
||||||
.iter()
|
"torrent",
|
||||||
.cloned(),
|
"create",
|
||||||
|
"--input",
|
||||||
|
"foo",
|
||||||
|
"--announce",
|
||||||
|
"udp:bar.com",
|
||||||
|
"--announce-tier",
|
||||||
|
"foo",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.cloned(),
|
||||||
);
|
);
|
||||||
fs::write(env.resolve("foo"), "").unwrap();
|
fs::write(env.resolve("foo"), "").unwrap();
|
||||||
env.status().ok();
|
env.status().ok();
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::common::*;
|
||||||
pub struct Metainfo {
|
pub struct Metainfo {
|
||||||
pub announce: String,
|
pub announce: String,
|
||||||
#[serde(rename = "announce list")]
|
#[serde(rename = "announce list")]
|
||||||
pub announce_list: Vec<Vec<String>>,
|
pub announce_list: Option<Vec<Vec<String>>>,
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
#[serde(rename = "created by")]
|
#[serde(rename = "created by")]
|
||||||
pub created_by: Option<String>,
|
pub created_by: Option<String>,
|
||||||
|
|
13
src/opt.rs
13
src/opt.rs
|
@ -1,23 +1,30 @@
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
use structopt::clap::{AppSettings, ArgSettings};
|
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
#[structopt(
|
#[structopt(
|
||||||
about(consts::ABOUT),
|
about(consts::ABOUT),
|
||||||
version(consts::VERSION),
|
version(consts::VERSION),
|
||||||
author(consts::AUTHOR),
|
author(consts::AUTHOR),
|
||||||
|
help_message(consts::HELP_MESSAGE),
|
||||||
|
version_message(consts::VERSION_MESSAGE),
|
||||||
global_setting(AppSettings::ColoredHelp),
|
global_setting(AppSettings::ColoredHelp),
|
||||||
global_setting(AppSettings::ColorAuto)
|
global_setting(AppSettings::ColorAuto)
|
||||||
)]
|
)]
|
||||||
pub(crate) struct Opt {
|
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,
|
unstable: bool,
|
||||||
#[structopt(
|
#[structopt(
|
||||||
long = "color",
|
long = "color",
|
||||||
default_value = use_color::AUTO,
|
default_value = use_color::AUTO,
|
||||||
set = ArgSettings::CaseInsensitive,
|
set = ArgSettings::CaseInsensitive,
|
||||||
possible_values = use_color::VALUES,
|
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,
|
pub(crate) use_color: UseColor,
|
||||||
#[structopt(subcommand)]
|
#[structopt(subcommand)]
|
||||||
|
|
|
@ -4,6 +4,11 @@ mod create;
|
||||||
mod stats;
|
mod stats;
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
|
#[structopt(
|
||||||
|
help_message(consts::HELP_MESSAGE),
|
||||||
|
version_message(consts::VERSION_MESSAGE),
|
||||||
|
about("Subcommands related to the BitTorrent protocol.")
|
||||||
|
)]
|
||||||
pub(crate) enum Torrent {
|
pub(crate) enum Torrent {
|
||||||
Create(torrent::create::Create),
|
Create(torrent::create::Create),
|
||||||
Stats(torrent::stats::Stats),
|
Stats(torrent::stats::Stats),
|
||||||
|
|
|
@ -1,26 +1,92 @@
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
|
#[structopt(
|
||||||
|
help_message(consts::HELP_MESSAGE),
|
||||||
|
version_message(consts::VERSION_MESSAGE),
|
||||||
|
about("Create a `.torrent` file.")
|
||||||
|
)]
|
||||||
pub(crate) struct Create {
|
pub(crate) struct Create {
|
||||||
#[structopt(name = "ANNOUNCE", long = "announce", required(true))]
|
#[structopt(
|
||||||
announce: Vec<String>,
|
name = "ANNOUNCE",
|
||||||
#[structopt(name = "COMMENT", long = "comment")]
|
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>,
|
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,
|
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,
|
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>,
|
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,
|
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,
|
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>,
|
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,
|
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,
|
private: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,11 +95,8 @@ impl Create {
|
||||||
let input = env.resolve(&self.input);
|
let input = env.resolve(&self.input);
|
||||||
|
|
||||||
let mut announce_list = Vec::new();
|
let mut announce_list = Vec::new();
|
||||||
for announce in &self.announce {
|
for tier in &self.announce_tiers {
|
||||||
let tier = announce
|
let tier = tier.split(',').map(str::to_string).collect::<Vec<String>>();
|
||||||
.split(',')
|
|
||||||
.map(str::to_string)
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
|
|
||||||
tier
|
tier
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -44,12 +107,6 @@ impl Create {
|
||||||
announce_list.push(tier);
|
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 {
|
let filename = input.file_name().ok_or_else(|| Error::FilenameExtract {
|
||||||
path: input.clone(),
|
path: input.clone(),
|
||||||
})?;
|
})?;
|
||||||
|
@ -106,8 +163,12 @@ impl Create {
|
||||||
let metainfo = Metainfo {
|
let metainfo = Metainfo {
|
||||||
comment: self.comment,
|
comment: self.comment,
|
||||||
encoding: consts::ENCODING_UTF8.to_string(),
|
encoding: consts::ENCODING_UTF8.to_string(),
|
||||||
announce,
|
announce: self.announce.to_string(),
|
||||||
announce_list,
|
announce_list: if announce_list.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(announce_list)
|
||||||
|
},
|
||||||
creation_date,
|
creation_date,
|
||||||
created_by,
|
created_by,
|
||||||
info,
|
info,
|
||||||
|
@ -180,7 +241,7 @@ mod tests {
|
||||||
fn tracker_flag_must_be_url() {
|
fn tracker_flag_must_be_url() {
|
||||||
let mut env = environment(&["--input", "foo", "--announce", "bar"]);
|
let mut env = environment(&["--input", "foo", "--announce", "bar"]);
|
||||||
fs::write(env.resolve("foo"), "").unwrap();
|
fs::write(env.resolve("foo"), "").unwrap();
|
||||||
assert_matches!(env.run(), Err(Error::AnnounceUrlParse { .. }));
|
assert_matches!(env.run(), Err(Error::Clap { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -191,22 +252,29 @@ mod tests {
|
||||||
let torrent = env.resolve("foo.torrent");
|
let torrent = env.resolve("foo.torrent");
|
||||||
let bytes = fs::read(torrent).unwrap();
|
let bytes = fs::read(torrent).unwrap();
|
||||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).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"]]);
|
assert!(metainfo.announce_list.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn announce_single_tier() {
|
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();
|
fs::write(env.resolve("foo"), "").unwrap();
|
||||||
env.run().unwrap();
|
env.run().unwrap();
|
||||||
let torrent = env.resolve("foo.torrent");
|
let torrent = env.resolve("foo.torrent");
|
||||||
let bytes = fs::read(torrent).unwrap();
|
let bytes = fs::read(torrent).unwrap();
|
||||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).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!(
|
assert_eq!(
|
||||||
metainfo.announce_list,
|
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",
|
"--input",
|
||||||
"foo",
|
"foo",
|
||||||
"--announce",
|
"--announce",
|
||||||
|
"http://bar",
|
||||||
|
"--announce-tier",
|
||||||
"http://bar,http://baz",
|
"http://bar,http://baz",
|
||||||
"--announce",
|
"--announce-tier",
|
||||||
"http://abc,http://xyz",
|
"http://abc,http://xyz",
|
||||||
]);
|
]);
|
||||||
fs::write(env.resolve("foo"), "").unwrap();
|
fs::write(env.resolve("foo"), "").unwrap();
|
||||||
|
@ -225,13 +295,13 @@ mod tests {
|
||||||
let torrent = env.resolve("foo.torrent");
|
let torrent = env.resolve("foo.torrent");
|
||||||
let bytes = fs::read(torrent).unwrap();
|
let bytes = fs::read(torrent).unwrap();
|
||||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).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!(
|
assert_eq!(
|
||||||
metainfo.announce_list,
|
metainfo.announce_list,
|
||||||
vec![
|
Some(vec![
|
||||||
vec!["http://bar", "http://baz"],
|
vec!["http://bar".into(), "http://baz".into()],
|
||||||
vec!["http://abc", "http://xyz"],
|
vec!["http://abc".into(), "http://xyz".into()],
|
||||||
]
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,42 @@
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[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 {
|
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>,
|
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>,
|
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,
|
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,
|
print: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +167,7 @@ impl Extractor {
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.print {
|
if self.print {
|
||||||
eprintln!("{}: {}", path.display(), value);
|
eprintln!("{}:\n{}", path.display(), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.extract(&value);
|
self.extract(&value);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user